diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index cc9f10b..bb9a945 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -16,6 +16,27 @@ const RE_WINDOWS_EXTENDED_PATH = /(\/\/\?\/|\\\\\?\\)/ const RE_WINDOWS_DRIVE_ROOT = /^:local:[a-zA-Z]:\/$/ const RE_WINDOWS_DRIVE_LETTER = /^[a-zA-Z]:$/ +export async function startDryRun(operation: () => Promise): Promise { + await rclone('/options/set', { + params: { + query: { + main: { DryRun: true }, + }, + }, + }) + try { + return await operation() + } finally { + await rclone('/options/set', { + params: { + query: { + main: { DryRun: false }, + }, + }, + }) + } +} + function serializeOptions( remotePath: string, options: { diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx index 7197150..242561a 100644 --- a/src/pages/Copy.tsx +++ b/src/pages/Copy.tsx @@ -12,7 +12,7 @@ import { } from '@heroui/react' import { useMutation } from '@tanstack/react-query' import { invoke } from '@tauri-apps/api/core' -import { message } from '@tauri-apps/plugin-dialog' +import { ask, message } from '@tauri-apps/plugin-dialog' import { platform } from '@tauri-apps/plugin-os' import cronstrue from 'cronstrue' import { AnimatePresence, motion } from 'framer-motion' @@ -20,6 +20,7 @@ import { AlertOctagonIcon, ClockIcon, CopyIcon, + EyeIcon, FilterIcon, FoldersIcon, PlayIcon, @@ -31,7 +32,7 @@ import { useSearchParams } from 'react-router-dom' import { getOptionsSubtitle } from '../../lib/flags' import { useFlags } from '../../lib/hooks' import notify from '../../lib/notify' -import { startCopy } from '../../lib/rclone/api' +import { startCopy, startDryRun } from '../../lib/rclone/api' import { RCLONE_CONFIG_DEFAULTS } from '../../lib/rclone/constants' import { openWindow } from '../../lib/window' import { useHostStore } from '../../store/host' @@ -178,6 +179,47 @@ export default function Copy() { }, }) + const dryRunMutation = useMutation({ + mutationFn: async () => { + if (!sources || sources.length === 0 || !dest) { + throw new Error('Please select both a source and destination path') + } + return startDryRun(() => + startCopy({ + sources, + destination: dest, + options: { + config: configOptions, + copy: copyOptions, + filter: filterOptions, + remotes: remoteOptions, + }, + }) + ) + }, + onSuccess: async () => { + const result = await ask( + 'Dry run started, you can check the results in the Transfers screen', + { + title: 'Preview (Dry Run)', + kind: 'info', + okLabel: 'Open Transfers', + cancelLabel: 'OK', + } + ) + if (result) { + await openWindow({ name: 'Transfers', url: '/transfers' }) + } + }, + onError: async (error) => { + console.error('Error starting dry run:', error) + await message(error instanceof Error ? error.message : 'Failed to start dry run', { + title: 'Dry Run', + kind: 'error', + }) + }, + }) + useEffect(() => { startTransition(() => { setConfigOptionsJsonString(JSON.stringify(RCLONE_CONFIG_DEFAULTS.config, null, 2)) @@ -535,6 +577,35 @@ export default function Copy() { )} + + + + + +