diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index bb9a945..32da5c7 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -392,7 +392,13 @@ async function fetchTransferred() { return transferred } -async function fetchJob(jobId: number, transferred: Awaited>) { +async function fetchJob( + jobId: number, + transferred: Awaited>, + 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 !!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 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, }) } } diff --git a/src/components/JobDetailsDrawer.tsx b/src/components/JobDetailsDrawer.tsx index 3b0220f..615dc11 100644 --- a/src/components/JobDetailsDrawer.tsx +++ b/src/components/JobDetailsDrawer.tsx @@ -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 ( @@ -169,6 +173,48 @@ export default function JobDetailsDrawer({ ) : null} + {selectedJob.isDryRun && ( + + This is a dry-run operation. No files were actually transferred. + + )} + + {checking.length > 0 ? ( +
+
+

+ + Checking +

+ {jobGroupStatsQuery.isLoading ? ( + + ) : ( + + {checking.length} item{checking.length === 1 ? '' : 's'} + + )} +
+

+ Files are being verified before transfer. This can take a while for + large directories. +

+ {checking.map((item, itemIndex) => { + const size = item.size ? formatBytes(item.size) : 'Unknown size' + return ( +
+

{item.name}

+

+ {size} +

+
+ ) + })} +
+ ) : null} +

Transferring

diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx index ea76488..1ff369b 100644 --- a/src/pages/Copy.tsx +++ b/src/pages/Copy.tsx @@ -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, diff --git a/src/pages/Delete.tsx b/src/pages/Delete.tsx index ead1f50..7d0fb80 100644 --- a/src/pages/Delete.tsx +++ b/src/pages/Delete.tsx @@ -210,7 +210,7 @@ export default function Delete() { sources: [sourceFs], options: { filter: filterOptions, - config: configOptions, + config: { ...configOptions, dry_run: true }, }, }) ) diff --git a/src/pages/Move.tsx b/src/pages/Move.tsx index 5f3866b..bea043b 100644 --- a/src/pages/Move.tsx +++ b/src/pages/Move.tsx @@ -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, diff --git a/src/pages/Sync.tsx b/src/pages/Sync.tsx index 1623c47..73eae60 100644 --- a/src/pages/Sync.tsx +++ b/src/pages/Sync.tsx @@ -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, diff --git a/src/pages/Transfers.tsx b/src/pages/Transfers.tsx index 43beba0..4549b20 100644 --- a/src/pages/Transfers.tsx +++ b/src/pages/Transfers.tsx @@ -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) => -
+
{job.hasError &&

ERROR: Tap to view details.

} {!job.hasError && ( @@ -169,11 +169,37 @@ function JobCard({ job, onSelect }: { job: JobItem; onSelect: (job: JobItem) => ) : null ) : null} + {job.isDryRun && ( + + DRY RUN + + )} + + {job.type === 'active' && job.isChecking ? ( + + } + > + Checking {job.checkingCount} + + + ) : null} + {job.type === 'active' ? ( - + ) : null} diff --git a/types/jobs.d.ts b/types/jobs.d.ts index d2a7ec6..9633d5c 100644 --- a/types/jobs.d.ts +++ b/types/jobs.d.ts @@ -8,4 +8,7 @@ export type JobItem = { progress: number hasError: boolean sources: string[] + isChecking: boolean + checkingCount: number + isDryRun?: boolean }