checking job state, #141

This commit is contained in:
FTCHD
2026-02-03 00:37:52 +07:00
parent f0cc98847f
commit e5d918e26f
8 changed files with 141 additions and 21 deletions
+58 -13
View File
@@ -392,7 +392,13 @@ async function fetchTransferred() {
return transferred
}
async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fetchTransferred>>) {
async function fetchJob(
jobId: number,
transferred: Awaited<ReturnType<typeof fetchTransferred>>,
checkingItems: { group?: string; name?: string; size?: number }[]
) {
console.log('[fetchJob] fetching job', jobId)
const job = await rclone('/core/stats', {
params: {
query: {
@@ -412,20 +418,33 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
console.log('[fetchJob] job status', jobId, JSON.stringify(jobStatus, null, 2))
let hasError = !!jobStatus?.error
let isDryRun = false
if (
!hasError &&
jobStatus.output &&
typeof jobStatus.output === 'object' &&
'results' in jobStatus.output &&
Array.isArray(jobStatus.output.results)
) {
hasError = jobStatus.output.results.some((result: any) => !!result?.error)
if (!hasError) {
hasError = jobStatus.output.results.some((result: any) => !!result?.error)
}
isDryRun = jobStatus.output.results.some((result: any) => {
const srcFs = result.input?.srcFs || ''
return srcFs.includes('dry_run') || srcFs.includes('dry-run')
})
}
const jobCheckingItems = checkingItems.filter((c) => c.group === `job/${jobId}`)
const isChecking = jobCheckingItems.length > 0
const checkingCount = jobCheckingItems.length
console.log('[fetchJob] checking state', jobId, { isChecking, checkingCount })
const relatedItems = transferred.filter((t) => t.group === `job/${jobId}`)
if (relatedItems.length === 0) {
console.log('[fetchJob] relatedItems not found', jobId)
if (relatedItems.length === 0 && !isChecking) {
console.log('[fetchJob] no relatedItems and not checking', jobId)
return null
}
@@ -455,6 +474,14 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
}
}
if (isChecking && sources.size === 0) {
for (const checkItem of jobCheckingItems) {
if (checkItem.name) {
sources.add(checkItem.name)
}
}
}
if (sources.size === 0 && !hasError) {
console.log('[fetchJob] source or hasError not found', jobId)
return null
@@ -467,10 +494,13 @@ async function fetchJob(jobId: number, transferred: Awaited<ReturnType<typeof fe
speed: job.speed,
done: job.bytes === job.totalBytes,
progress: Math.round((job.bytes / job.totalBytes) * 100),
progress: job.totalBytes > 0 ? Math.round((job.bytes / job.totalBytes) * 100) : 0,
hasError: hasError,
sources: Array.from(sources),
isChecking,
checkingCount,
isDryRun,
}
}
@@ -481,8 +511,10 @@ export async function listTransfers() {
console.log('[listTransfers] allStats', JSON.stringify(allStats, null, 2))
const transferring = allStats?.transferring || []
const checking = allStats?.checking || []
console.log('[listTransfers] transferring count:', transferring.length)
console.log('[listTransfers] checking count:', checking.length)
const transferred = await fetchTransferred()
console.log('[listTransfers] transferred count:', transferred?.length || 0)
@@ -492,19 +524,30 @@ export async function listTransfers() {
inactive: [] as JobItem[],
}
const activeJobIds = new Set(
const transferringJobIds = new Set(
transferring
?.filter((t) => t.group?.startsWith('job/'))
.filter((t) => t.group?.startsWith('job/'))
.map((t) => Number(t.group!.split('/')[1]))
.sort((a, b) => a - b)
)
console.log('[listTransfers] activeJobIds', activeJobIds.size)
const checkingJobIds = new Set(
checking
.filter((c) => c.group?.startsWith('job/'))
.map((c) => Number(c.group!.split('/')[1]))
)
const activeJobIds = new Set([...transferringJobIds, ...checkingJobIds])
const sortedActiveJobIds = Array.from(activeJobIds).sort((a, b) => a - b)
console.log('[listTransfers] transferring job IDs:', Array.from(transferringJobIds))
console.log('[listTransfers] checking job IDs:', Array.from(checkingJobIds))
console.log('[listTransfers] combined active job IDs:', sortedActiveJobIds)
const isWindows = platform() === 'windows'
console.log('[listTransfers] isWindows', isWindows)
for (const jobId of activeJobIds) {
const job = await fetchJob(jobId, transferred)
for (const jobId of sortedActiveJobIds) {
const job = await fetchJob(jobId, transferred, checking)
if (job) {
jobs.active.push({
...job,
@@ -523,12 +566,14 @@ export async function listTransfers() {
console.log('[listTransfers] inactive job IDs:', Array.from(inactiveJobIds))
for (const jobId of inactiveJobIds) {
const job = await fetchJob(jobId, transferred)
const job = await fetchJob(jobId, transferred, checking)
if (job) {
jobs.inactive.push({
...job,
speed: 0,
type: 'inactive',
isChecking: false,
checkingCount: 0,
})
}
}
+47 -1
View File
@@ -14,7 +14,7 @@ import {
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { message } from '@tauri-apps/plugin-dialog'
import { platform } from '@tauri-apps/plugin-os'
import { SquareIcon } from 'lucide-react'
import { SearchCheckIcon, SquareIcon } from 'lucide-react'
import { useMemo } from 'react'
import { formatBytes } from '../../lib/format'
import notify from '../../lib/notify'
@@ -126,6 +126,10 @@ export default function JobDetailsDrawer({
() => jobGroupStatsQuery.data?.transferring || [],
[jobGroupStatsQuery.data]
)
const checking = useMemo(
() => jobGroupStatsQuery.data?.checking || [],
[jobGroupStatsQuery.data]
)
return (
<Drawer isOpen={isOpen} placement="bottom" size="2xl" onClose={onClose}>
@@ -169,6 +173,48 @@ export default function JobDetailsDrawer({
</Alert>
) : null}
{selectedJob.isDryRun && (
<Alert color="warning" variant="faded">
This is a dry-run operation. No files were actually transferred.
</Alert>
)}
{checking.length > 0 ? (
<div className="flex flex-col gap-2 pb-4">
<div className="flex flex-row items-center justify-between gap-2">
<h3 className="flex flex-row items-center gap-2 text-lg font-medium">
<SearchCheckIcon className="w-5 h-5 text-warning" />
Checking
</h3>
{jobGroupStatsQuery.isLoading ? (
<Spinner />
) : (
<Chip size="sm" color="warning" variant="flat">
{checking.length} item{checking.length === 1 ? '' : 's'}
</Chip>
)}
</div>
<p className="text-sm text-default-500">
Files are being verified before transfer. This can take a while for
large directories.
</p>
{checking.map((item, itemIndex) => {
const size = item.size ? formatBytes(item.size) : 'Unknown size'
return (
<div
key={item.name || itemIndex}
className="flex flex-row items-center justify-between gap-2 pb-2 border-b border-divider"
>
<p className="flex-1 line-clamp-1 min-w-80">{item.name}</p>
<p className="text-sm tabular-nums text-default-500">
{size}
</p>
</div>
)
})}
</div>
) : null}
<div className="flex flex-col gap-2">
<div className="flex flex-row items-center justify-between gap-2">
<h3 className="text-lg font-medium">Transferring</h3>
+1 -1
View File
@@ -189,7 +189,7 @@ export default function Copy() {
sources,
destination: dest,
options: {
config: configOptions,
config: { ...configOptions, dry_run: true },
copy: copyOptions,
filter: filterOptions,
remotes: remoteOptions,
+1 -1
View File
@@ -210,7 +210,7 @@ export default function Delete() {
sources: [sourceFs],
options: {
filter: filterOptions,
config: configOptions,
config: { ...configOptions, dry_run: true },
},
})
)
+1 -1
View File
@@ -216,7 +216,7 @@ export default function Move() {
sources,
destination: dest,
options: {
config: configOptions,
config: { ...configOptions, dry_run: true },
move: moveOptions,
filter: filterOptions,
remotes: remoteOptions,
+1 -1
View File
@@ -183,7 +183,7 @@ export default function Sync() {
source,
destination: dest,
options: {
config: configOptions,
config: { ...configOptions, dry_run: true },
sync: syncOptions,
filter: filterOptions,
remotes: remoteOptions,
+29 -3
View File
@@ -2,7 +2,7 @@ import { Card, CardBody, Progress, Tab, Tabs, Tooltip, useDisclosure } from '@he
import { Button, Chip, Spinner } from '@heroui/react'
import { useQuery } from '@tanstack/react-query'
import { message } from '@tauri-apps/plugin-dialog'
import { ChevronRightIcon, RefreshCcwIcon } from 'lucide-react'
import { ChevronRightIcon, RefreshCcwIcon, SearchCheckIcon } from 'lucide-react'
import { startTransition, useCallback, useMemo, useState } from 'react'
import { buildReadablePathMultiple, formatBytes } from '../../lib/format'
import { listTransfers } from '../../lib/rclone/api'
@@ -154,7 +154,7 @@ function JobCard({ job, onSelect }: { job: JobItem; onSelect: (job: JobItem) =>
</Chip>
</Tooltip>
<div className="flex flex-row items-center flex-1">
<div className="flex flex-row items-center flex-1 gap-2">
{job.hasError && <p className="text-danger">ERROR: Tap to view details.</p>}
{!job.hasError && (
@@ -169,11 +169,37 @@ function JobCard({ job, onSelect }: { job: JobItem; onSelect: (job: JobItem) =>
) : null
) : null}
{job.isDryRun && (
<Chip size="sm" variant="flat" color="warning">
DRY RUN
</Chip>
)}
{job.type === 'active' && job.isChecking ? (
<Tooltip
content={`Checking ${job.checkingCount} file${job.checkingCount === 1 ? '' : 's'} before transfer`}
color="foreground"
>
<Chip
size="sm"
variant="flat"
color="warning"
startContent={<SearchCheckIcon className="w-3 h-3" />}
>
Checking {job.checkingCount}
</Chip>
</Tooltip>
) : null}
{job.type === 'active' ? (
<Tooltip
content={`${formatBytes(job.bytes)} out of ${formatBytes(job.totalBytes)}`}
>
<Progress value={job.progress} isStriped={true} />
<Progress
value={job.progress}
isStriped={true}
isIndeterminate={job.isChecking && job.totalBytes === 0}
/>
</Tooltip>
) : null}
</>
+3
View File
@@ -8,4 +8,7 @@ export type JobItem = {
progress: number
hasError: boolean
sources: string[]
isChecking: boolean
checkingCount: number
isDryRun?: boolean
}