checking job state, #141
This commit is contained in:
+58
-13
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function Delete() {
|
||||
sources: [sourceFs],
|
||||
options: {
|
||||
filter: filterOptions,
|
||||
config: configOptions,
|
||||
config: { ...configOptions, dry_run: true },
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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}
|
||||
</>
|
||||
|
||||
Vendored
+3
@@ -8,4 +8,7 @@ export type JobItem = {
|
||||
progress: number
|
||||
hasError: boolean
|
||||
sources: string[]
|
||||
isChecking: boolean
|
||||
checkingCount: number
|
||||
isDryRun?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user