diff --git a/lib/rclone/api.ts b/lib/rclone/api.ts index a4270d1..7b041d7 100644 --- a/lib/rclone/api.ts +++ b/lib/rclone/api.ts @@ -369,11 +369,15 @@ export async function mountRemote({ mountPoint, mountOptions, vfsOptions, + _filter, + _config, }: { remotePath: string mountPoint: string mountOptions?: Record vfsOptions?: Record + _filter?: Record + _config?: Record }) { console.log('[mountRemote]', remotePath, mountPoint) @@ -389,6 +393,14 @@ export async function mountRemote({ options.set('vfsOpt', JSON.stringify(parseRcloneOptions(vfsOptions))) } + if (_filter && Object.keys(_filter).length > 0) { + options.set('_filter', JSON.stringify(parseRcloneOptions(_filter))) + } + + if (_config && Object.keys(_config).length > 0) { + options.set('_config', JSON.stringify(parseRcloneOptions(_config))) + } + const r = await fetch(`http://localhost:5572/mount/mount?${options.toString()}`, { method: 'POST', headers: getAuthHeader(), @@ -594,10 +606,12 @@ export async function startDelete({ fs, rmDirs, _filter, + _config, }: { fs: string rmDirs?: boolean // delete empty src directories if set _filter?: Record + _config?: Record }) { console.log('[startDelete]', fs, rmDirs) @@ -614,6 +628,10 @@ export async function startDelete({ params.set('_filter', JSON.stringify(parseRcloneOptions(_filter))) } + if (_config && Object.keys(_config).length > 0) { + params.set('_config', JSON.stringify(parseRcloneOptions(_config))) + } + const r = await fetch(`http://localhost:5572/operations/delete?${params.toString()}`, { method: 'POST', headers: getAuthHeader(), @@ -627,9 +645,8 @@ export async function startDelete({ } /* FLAGS */ - -export async function getGlobalFlags() { - console.log('[getGlobalFlags]') +export async function getCurrentGlobalFlags() { + console.log('[getCurrentGlobalFlags]') const r = await fetch('http://localhost:5572/options/get', { method: 'POST', @@ -649,9 +666,9 @@ export async function getCopyFlags() { const mainFlags = r.main - const copyFlags = mainFlags.filter( - (flag: any) => flag?.Groups?.includes('Copy') || flag?.Groups?.includes('Performance') - ) + const copyFlags = mainFlags + .filter((flag: any) => flag?.Groups?.includes('Copy')) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) return copyFlags } @@ -666,12 +683,9 @@ export async function getSyncFlags() { const mainFlags = r.main - const syncFlags = mainFlags.filter( - (flag: any) => - flag?.Groups?.includes('Copy') || - flag?.Groups?.includes('Sync') || - flag?.Groups?.includes('Performance') - ) + const syncFlags = mainFlags + .filter((flag: any) => flag?.Groups?.includes('Copy') || flag?.Groups?.includes('Sync')) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) return syncFlags } @@ -687,7 +701,9 @@ export async function getFilterFlags() { const filterFlags = r.filter // ignore "Metadata" fields as they have the same FieldNames as the normal non-metadata filters - const filteredFlags = filterFlags.filter((flag: any) => !flag.Groups.includes('Metadata')) + const filteredFlags = filterFlags + .filter((flag: any) => !flag.Groups.includes('Metadata')) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) return filteredFlags } @@ -704,7 +720,9 @@ export async function getVfsFlags() { const IGNORED_FLAGS = ['NONE'] - const filteredFlags = vfsFlags.filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name)) + const filteredFlags = vfsFlags + .filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name)) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) return filteredFlags } @@ -721,7 +739,33 @@ export async function getMountFlags() { const IGNORED_FLAGS = ['debug_fuse', 'daemon', 'daemon_timeout'] - const filteredFlags = mountFlags.filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name)) + const filteredFlags = mountFlags + .filter((flag: any) => !IGNORED_FLAGS.includes(flag.Name)) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) return filteredFlags } + +export async function getConfigFlags() { + console.log('[getConfigFlags]') + + const r = await fetch('http://localhost:5572/options/info', { + method: 'POST', + headers: getAuthHeader(), + }).then((res) => res.json() as Promise) + + const mainFlags = r.main + + const copyFlags = mainFlags + .filter( + (flag: any) => + flag?.Groups?.includes('Performance') || + flag?.Groups?.includes('Listing') || + flag?.Groups?.includes('Networking') || + flag?.Groups?.includes('Check') || + flag?.Name === 'use_server_modtime' + ) + .sort((a: any, b: any) => a.Name.localeCompare(b.Name)) + + return copyFlags +} diff --git a/lib/rclone/constants.ts b/lib/rclone/constants.ts new file mode 100644 index 0000000..f56a60c --- /dev/null +++ b/lib/rclone/constants.ts @@ -0,0 +1,15 @@ +export const RCLONE_CONFIG_DEFAULTS = { + 'MultiThreadCutoff': '64M', + 'MultiThreadStreams': '8M', + 'MultiThreadChunkSize': '8M', + 'UseListR': 'true', + 'UseServerModTime': 'true', + 'BufferSize': '32M', + 'Transfers': '8', + 'Checkers': '16', +} + +export const RCLONE_VFS_DEFAULTS = { + 'ChunkSize': '4M', + 'ChunkStreams': '16', +} diff --git a/lib/store.ts b/lib/store.ts index e8364c6..4c29198 100644 --- a/lib/store.ts +++ b/lib/store.ts @@ -21,6 +21,8 @@ export interface RemoteConfig { copyDefaults?: Record moveDefaults?: Record syncDefaults?: Record + configDefaults?: Record + remoteDefaults?: Record } interface State { diff --git a/src/components/OptionsSection.tsx b/src/components/OptionsSection.tsx index 797b759..3d29064 100644 --- a/src/components/OptionsSection.tsx +++ b/src/components/OptionsSection.tsx @@ -7,7 +7,7 @@ export default function OptionsSection({ optionsJson, setOptionsJson, globalOptions, - optionsFetcher, + getAvailableOptions, rows = 14, isLocked, setIsLocked, @@ -15,24 +15,26 @@ export default function OptionsSection({ optionsJson: string setOptionsJson: (value: string) => void globalOptions: any[] - optionsFetcher: () => Promise + getAvailableOptions: () => Promise rows?: number isLocked?: boolean setIsLocked?: (value: boolean) => void }) { - const [copyAvailableOptions, setCopyAvailableOptions] = useState([]) + const [availableOptions, setAvailableOptions] = useState([]) const [options, setOptions] = useState({}) const [isJsonValid, setIsJsonValid] = useState(true) + console.log('[OptionsSection] globalOptions', globalOptions) + useEffect(() => { - optionsFetcher() + getAvailableOptions() .then((flags) => { console.log(JSON.stringify(flags, null, 2)) return flags }) - .then((flags) => setCopyAvailableOptions(flags)) - }, [optionsFetcher]) + .then((flags) => setAvailableOptions(flags)) + }, [getAvailableOptions]) useEffect(() => { try { @@ -93,7 +95,11 @@ export default function OptionsSection({ }} endContent={ setIsLocked && ( - + {isLocked ? (
- {copyAvailableOptions.map((option) => { + {availableOptions.map((option) => { const alreadyAdded = isOptionAdded(option.FieldName) return ( @@ -120,10 +126,20 @@ export default function OptionsSection({ key={option.FieldName} delay={500} content={ - copyAvailableOptions.find((o) => o.FieldName === option.FieldName) - ?.Help +
+

{option.Name}

+

+ { + availableOptions.find( + (o) => o.FieldName === option.FieldName + )?.Help + } +

+
} closeDelay={0} + className="pt-1.5 max-w-52" + color="foreground" > o.FieldName === option.FieldName )?.DefaultStr || '' @@ -159,7 +175,9 @@ export default function OptionsSection({ className="cursor-pointer" size="sm" endContent={ - alreadyAdded ? : undefined + alreadyAdded ? ( + + ) : undefined } > {option.FieldName} diff --git a/src/components/PathFinder.tsx b/src/components/PathFinder.tsx index fa8faa0..153c098 100644 --- a/src/components/PathFinder.tsx +++ b/src/components/PathFinder.tsx @@ -180,7 +180,7 @@ export function MultiPathFinder({ {switchable && (
- +
+ + } + /> + } + indicator={} + subtitle={`Default config flags for ${remoteName}`} + title="Config" + > + + @@ -490,7 +523,7 @@ export default function RemoteDefaultsDrawer({ globalOptions={ globalOptions['filter' as keyof typeof globalOptions] } - optionsFetcher={getFilterFlags} + getAvailableOptions={getFilterFlags} rows={4} /> @@ -513,7 +546,7 @@ export default function RemoteDefaultsDrawer({ globalOptions={ globalOptions['main' as keyof typeof globalOptions] } - optionsFetcher={getCopyFlags} + getAvailableOptions={getCopyFlags} /> @@ -536,7 +569,7 @@ export default function RemoteDefaultsDrawer({ globalOptions={ globalOptions['main' as keyof typeof globalOptions] } - optionsFetcher={getSyncFlags} + getAvailableOptions={getSyncFlags} rows={20} /> diff --git a/src/pages/Copy.tsx b/src/pages/Copy.tsx index 6aba534..39807ab 100644 --- a/src/pages/Copy.tsx +++ b/src/pages/Copy.tsx @@ -10,12 +10,20 @@ import { FilterIcon, FoldersIcon, PlayIcon, + WrenchIcon, } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { getRemoteName } from '../../lib/format' import { isRemotePath } from '../../lib/fs' -import { getCopyFlags, getFilterFlags, getGlobalFlags, startCopy } from '../../lib/rclone/api' +import { + getConfigFlags, + getCopyFlags, + getCurrentGlobalFlags, + getFilterFlags, + startCopy, +} from '../../lib/rclone/api' +import { RCLONE_CONFIG_DEFAULTS } from '../../lib/rclone/constants' import { usePersistedStore } from '../../lib/store' import { openWindow } from '../../lib/window' import CronEditor from '../components/CronEditor' @@ -32,7 +40,7 @@ export default function Copy() { const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [jsonError, setJsonError] = useState<'copy' | 'filter' | null>(null) + const [jsonError, setJsonError] = useState<'copy' | 'filter' | 'config' | null>(null) const [copyOptionsLocked, setCopyOptionsLocked] = useState(false) const [copyOptions, setCopyOptions] = useState>({}) @@ -42,9 +50,31 @@ export default function Copy() { const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') + const [configOptionsLocked, setConfigOptionsLocked] = useState(false) + const [configOptions, setConfigOptions] = useState>({}) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') + + // const [remoteOptionsLocked, setRemoteOptionsLocked] = useState(false) + // const [remoteOptions, setRemoteOptions] = useState>({}) + // const [remoteOptionsJson, setRemoteOptionsJson] = useState('{}') + const [cronExpression, setCronExpression] = useState(null) - const [globalOptions, setGlobalOptions] = useState([]) + const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) + + // const [backends, setBackends] = useState([]) + // useEffect(() => { + // getBackends().then((b) => { + // setBackends(b) + // }) + // }, []) + + // console.log('sources', sources) + // console.log('dest', dest) + + useEffect(() => { + getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags)) + }, []) // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { @@ -55,6 +85,7 @@ export default function Copy() { let mergedCopyDefaults = {} let mergedFilterDefaults = {} + let mergedConfigDefaults = {} // Helper function to merge defaults from a remote const mergeRemoteDefaults = (remote: string | null) => { @@ -75,6 +106,18 @@ export default function Copy() { ...remoteConfig.filterDefaults, } } + + if (remoteConfig.configDefaults) { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...remoteConfig.configDefaults, + } + } else { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...RCLONE_CONFIG_DEFAULTS, + } + } } // Only merge defaults for remote paths @@ -88,26 +131,29 @@ export default function Copy() { if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) } + + if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) { + setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2)) + } }, [sources, dest]) useEffect(() => { - getGlobalFlags().then((flags) => setGlobalOptions(flags)) - }, []) - - useEffect(() => { - let step: 'copy' | 'filter' = 'copy' + let step: 'copy' | 'filter' | 'config' = 'copy' try { setCopyOptions(JSON.parse(copyOptionsJson)) step = 'filter' setFilterOptions(JSON.parse(filterOptionsJson)) + step = 'config' + setConfigOptions(JSON.parse(configOptionsJson)) + setJsonError(null) } catch (error) { setJsonError(step) console.error(`Error parsing ${step} options:`, error) } - }, [copyOptionsJson, filterOptionsJson]) + }, [copyOptionsJson, filterOptionsJson, configOptionsJson]) const handleStartCopy = useCallback(async () => { setIsLoading(true) @@ -169,6 +215,11 @@ export default function Copy() { ) } + const mergedConfig = { + ...configOptions, + ...copyOptions, + } + if (cronExpression) { if (sources.length > 1) { await message( @@ -192,13 +243,13 @@ export default function Copy() { return } usePersistedStore.getState().addScheduledTask({ - 'type': 'copy', - 'cron': cronExpression, - 'args': { - 'srcFs': sources[0], - 'dstFs': dest, - '_config': copyOptions, - '_filter': filterOptions, + type: 'copy', + cron: cronExpression, + args: { + srcFs: sources[0], + dstFs: dest, + _config: mergedConfig, + _filter: filterOptions, }, }) } @@ -220,7 +271,7 @@ export default function Copy() { const jobId = await startCopy({ srcFs: customSource, dstFs: dest, - _config: copyOptions, + _config: mergedConfig, _filter: customFilterOptions, }) @@ -293,7 +344,7 @@ export default function Copy() { } setIsLoading(false) - }, [sources, dest, copyOptions, filterOptions, cronExpression]) + }, [sources, dest, copyOptions, filterOptions, cronExpression, configOptions]) const buttonText = useMemo(() => { if (isLoading) return 'STARTING...' @@ -335,10 +386,12 @@ export default function Copy() { title="Copy" > + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + + {buttonText} diff --git a/src/pages/Delete.tsx b/src/pages/Delete.tsx index acbebbb..d096718 100644 --- a/src/pages/Delete.tsx +++ b/src/pages/Delete.tsx @@ -1,11 +1,24 @@ import { Accordion, AccordionItem, Avatar, Button } from '@heroui/react' import { message } from '@tauri-apps/plugin-dialog' import cronstrue from 'cronstrue' -import { AlertOctagonIcon, ClockIcon, FilterIcon, FoldersIcon, PlayIcon } from 'lucide-react' +import { + AlertOctagonIcon, + ClockIcon, + FilterIcon, + FoldersIcon, + PlayIcon, + WrenchIcon, +} from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { getRemoteName } from '../../lib/format' -import { getFilterFlags, getGlobalFlags, startDelete } from '../../lib/rclone/api' +import { + getConfigFlags, + getCurrentGlobalFlags, + getFilterFlags, + startDelete, +} from '../../lib/rclone/api' +import { RCLONE_CONFIG_DEFAULTS } from '../../lib/rclone/constants' import { usePersistedStore } from '../../lib/store' import CronEditor from '../components/CronEditor' import OptionsSection from '../components/OptionsSection' @@ -24,13 +37,17 @@ export default function Delete() { const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [jsonError, setJsonError] = useState<'filter' | null>(null) + const [jsonError, setJsonError] = useState<'filter' | 'config' | null>(null) const [filterOptionsLocked, setFilterOptionsLocked] = useState(false) const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') - const [globalOptions, setGlobalOptions] = useState([]) + const [configOptionsLocked, setConfigOptionsLocked] = useState(false) + const [configOptions, setConfigOptions] = useState>({}) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') + + const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { @@ -39,6 +56,7 @@ export default function Delete() { const sourceRemote = getRemoteName(sourceFs?.[0]) let mergedFilterDefaults = {} + let mergedConfigDefaults = {} // Helper function to merge defaults from a remote const mergeRemoteDefaults = (remote: string | null) => { @@ -52,6 +70,18 @@ export default function Delete() { ...remoteConfig.filterDefaults, } } + + if (remoteConfig.configDefaults) { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...remoteConfig.configDefaults, + } + } else { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...RCLONE_CONFIG_DEFAULTS, + } + } } // Only merge defaults for remote paths @@ -60,22 +90,30 @@ export default function Delete() { if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) } + + if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) { + setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2)) + } }, [sourceFs]) useEffect(() => { - getGlobalFlags().then((flags) => setGlobalOptions(flags)) + getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags)) }, []) useEffect(() => { + let step: 'filter' | 'config' = 'filter' try { setFilterOptions(JSON.parse(filterOptionsJson)) + step = 'config' + setConfigOptions(JSON.parse(configOptionsJson)) + setJsonError(null) } catch (error) { - setJsonError('filter') - console.error('Error parsing filter options:', error) + setJsonError(step) + console.error(`Error parsing ${step} options:`, error) } - }, [filterOptionsJson]) + }, [filterOptionsJson, configOptionsJson]) const handleStartDelete = useCallback(async () => { setIsLoading(true) @@ -100,12 +138,13 @@ export default function Delete() { return } usePersistedStore.getState().addScheduledTask({ - 'type': 'delete', - 'cron': cronExpression, - 'args': { - 'fs': sourceFs, - 'rmDirs': rmDirs, - '_filter': filterOptions, + type: 'delete', + cron: cronExpression, + args: { + fs: sourceFs, + rmDirs: rmDirs, + _filter: filterOptions, + _config: configOptions, }, }) } @@ -115,6 +154,7 @@ export default function Delete() { fs: sourceFs, rmDirs, _filter: filterOptions, + _config: configOptions, }) setIsStarted(true) @@ -132,7 +172,7 @@ export default function Delete() { } finally { setIsLoading(false) } - }, [sourceFs, filterOptions, rmDirs, cronExpression]) + }, [sourceFs, filterOptions, rmDirs, cronExpression, configOptions]) const buttonText = useMemo(() => { if (isLoading) return 'STARTING...' @@ -178,15 +218,38 @@ export default function Delete() { title="Filters" > + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + + (null) + const [jsonError, setJsonError] = useState<'mount' | 'vfs' | 'filter' | 'config' | null>(null) const [mountOptionsLocked, setMountOptionsLocked] = useState(false) const [mountOptions, setMountOptions] = useState>({}) @@ -42,7 +52,15 @@ export default function Mount() { const [vfsOptions, setVfsOptions] = useState>({}) const [vfsOptionsJson, setVfsOptionsJson] = useState('{}') - const [globalOptions, setGlobalOptions] = useState([]) + const [filterOptionsLocked, setFilterOptionsLocked] = useState(false) + const [filterOptions, setFilterOptions] = useState>({}) + const [filterOptionsJson, setFilterOptionsJson] = useState('{}') + + const [configOptionsLocked, setConfigOptionsLocked] = useState(false) + const [configOptions, setConfigOptions] = useState>({}) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') + + const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { @@ -64,35 +82,67 @@ export default function Mount() { ) } + if (!vfsOptionsLocked) { + if ( + storeData.remoteConfigList[remote].vfsDefaults && + Object.keys(storeData.remoteConfigList[remote].vfsDefaults).length > 0 + ) { + setVfsOptionsJson( + JSON.stringify(storeData.remoteConfigList[remote].vfsDefaults, null, 2) + ) + } else { + setVfsOptionsJson(JSON.stringify(RCLONE_VFS_DEFAULTS, null, 2)) + } + } + if ( - storeData.remoteConfigList[remote].vfsDefaults && - Object.keys(storeData.remoteConfigList[remote].vfsDefaults).length > 0 && - !vfsOptionsLocked + storeData.remoteConfigList[remote].filterDefaults && + Object.keys(storeData.remoteConfigList[remote].filterDefaults).length > 0 && + !filterOptionsLocked ) { - setVfsOptionsJson( - JSON.stringify(storeData.remoteConfigList[remote].vfsDefaults, null, 2) + setFilterOptionsJson( + JSON.stringify(storeData.remoteConfigList[remote].filterDefaults, null, 2) ) } + + if (!configOptionsLocked) { + if ( + storeData.remoteConfigList[remote].configDefaults && + Object.keys(storeData.remoteConfigList[remote].configDefaults).length > 0 + ) { + setConfigOptionsJson( + JSON.stringify(storeData.remoteConfigList[remote].configDefaults, null, 2) + ) + } else { + setConfigOptionsJson(JSON.stringify(RCLONE_CONFIG_DEFAULTS, null, 2)) + } + } }, [source]) useEffect(() => { - getGlobalFlags().then((flags) => setGlobalOptions(flags)) + getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags)) }, []) useEffect(() => { - let step: 'mount' | 'vfs' = 'mount' + let step: 'mount' | 'vfs' | 'filter' | 'config' = 'mount' try { setMountOptions(JSON.parse(mountOptionsJson)) step = 'vfs' setVfsOptions(JSON.parse(vfsOptionsJson)) + step = 'filter' + setFilterOptions(JSON.parse(filterOptionsJson)) + + step = 'config' + setConfigOptions(JSON.parse(configOptionsJson)) + setJsonError(null) } catch (error) { setJsonError(step) console.error(`[Mount] Error parsing ${step} options:`, error) } - }, [mountOptionsJson, vfsOptionsJson]) + }, [mountOptionsJson, vfsOptionsJson, filterOptionsJson, configOptionsJson]) const handleStartMount = useCallback(async () => { if (!dest || !source) return @@ -110,7 +160,10 @@ export default function Mount() { const _mountOptions = { ...mountOptions } - if (!('VolumeName' in _mountOptions) && ['windows', 'macos'].includes(platform())) { + if ( + (!('VolumeName' in _mountOptions) || !_mountOptions.VolumeName) && + ['windows', 'macos'].includes(platform()) + ) { _mountOptions.VolumeName = `${source.split(sep()).pop()}${Math.random().toString(36).substring(2, 3).toUpperCase()}` } @@ -162,6 +215,8 @@ export default function Mount() { mountPoint: dest, mountOptions: _mountOptions, vfsOptions, + _filter: filterOptions, + _config: configOptions, }) setIsMounted(true) @@ -176,7 +231,7 @@ export default function Mount() { } finally { setIsLoading(false) } - }, [source, dest, mountOptions, vfsOptions]) + }, [source, dest, mountOptions, vfsOptions, filterOptions, configOptions]) const buttonText = useMemo(() => { if (isLoading) return 'MOUNTING...' @@ -235,8 +290,10 @@ export default function Mount() { + } /> + } + indicator={} + subtitle="Tap to toggle filtering options for this operation" + title="Filters" + > + + + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + +
diff --git a/src/pages/Move.tsx b/src/pages/Move.tsx index d4bca74..0b28fc2 100644 --- a/src/pages/Move.tsx +++ b/src/pages/Move.tsx @@ -10,12 +10,20 @@ import { FoldersIcon, MoveIcon, PlayIcon, + WrenchIcon, } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { getRemoteName } from '../../lib/format' import { isRemotePath } from '../../lib/fs' -import { getCopyFlags, getFilterFlags, getGlobalFlags, startMove } from '../../lib/rclone/api' +import { + getConfigFlags, + getCopyFlags, + getCurrentGlobalFlags, + getFilterFlags, + startMove, +} from '../../lib/rclone/api' +import { RCLONE_CONFIG_DEFAULTS } from '../../lib/rclone/constants' import { usePersistedStore } from '../../lib/store' import { openWindow } from '../../lib/window' import CronEditor from '../components/CronEditor' @@ -34,7 +42,7 @@ export default function Move() { const [isStarted, setIsStarted] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [jsonError, setJsonError] = useState<'move' | 'filter' | null>(null) + const [jsonError, setJsonError] = useState<'move' | 'filter' | 'config' | null>(null) const [moveOptionsLocked, setMoveOptionsLocked] = useState(false) const [moveOptions, setMoveOptions] = useState>({}) @@ -44,9 +52,17 @@ export default function Move() { const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') + const [configOptionsLocked, setConfigOptionsLocked] = useState(false) + const [configOptions, setConfigOptions] = useState>({}) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') + const [cronExpression, setCronExpression] = useState(null) - const [globalOptions, setGlobalOptions] = useState([]) + const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) + + useEffect(() => { + getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags)) + }, []) // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { @@ -57,6 +73,7 @@ export default function Move() { let mergedMoveDefaults = {} let mergedFilterDefaults = {} + let mergedConfigDefaults = {} // Helper function to merge defaults from a remote const mergeRemoteDefaults = (remote: string | null) => { @@ -77,6 +94,18 @@ export default function Move() { ...remoteConfig.filterDefaults, } } + + if (remoteConfig.configDefaults) { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...remoteConfig.configDefaults, + } + } else { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...RCLONE_CONFIG_DEFAULTS, + } + } } // Only merge defaults for remote paths @@ -90,26 +119,29 @@ export default function Move() { if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) } + + if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) { + setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2)) + } }, [sources, dest]) useEffect(() => { - getGlobalFlags().then((flags) => setGlobalOptions(flags)) - }, []) - - useEffect(() => { - let step: 'move' | 'filter' = 'move' + let step: 'move' | 'filter' | 'config' = 'move' try { setMoveOptions(JSON.parse(moveOptionsJson)) step = 'filter' setFilterOptions(JSON.parse(filterOptionsJson)) + step = 'config' + setConfigOptions(JSON.parse(configOptionsJson)) + setJsonError(null) } catch (error) { setJsonError(step) console.error(`Error parsing ${step} options:`, error) } - }, [moveOptionsJson, filterOptionsJson]) + }, [moveOptionsJson, filterOptionsJson, configOptionsJson]) const handleStartMove = useCallback(async () => { setIsLoading(true) @@ -171,6 +203,11 @@ export default function Move() { ) } + const mergedConfig = { + ...configOptions, + ...moveOptions, + } + if (cronExpression) { if (sources.length > 1) { await message( @@ -194,15 +231,15 @@ export default function Move() { return } usePersistedStore.getState().addScheduledTask({ - 'type': 'move', - 'cron': cronExpression, - 'args': { - 'srcFs': sources[0], - 'dstFs': dest, - 'createEmptySrcDirs': createEmptySrcDirs, - 'deleteEmptyDstDirs': deleteEmptyDstDirs, - '_config': moveOptions, - '_filter': filterOptions, + type: 'move', + cron: cronExpression, + args: { + srcFs: sources[0], + dstFs: dest, + createEmptySrcDirs, + deleteEmptyDstDirs, + _config: mergedConfig, + _filter: filterOptions, }, }) } @@ -226,7 +263,7 @@ export default function Move() { dstFs: dest, createEmptySrcDirs, deleteEmptyDstDirs, - _config: moveOptions, + _config: mergedConfig, _filter: customFilterOptions, }) @@ -307,6 +344,7 @@ export default function Move() { cronExpression, createEmptySrcDirs, deleteEmptyDstDirs, + configOptions, ]) const buttonText = useMemo(() => { @@ -359,10 +397,12 @@ export default function Move() { title="Move" > + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + + (null) + const [jsonError, setJsonError] = useState<'sync' | 'filter' | 'config' | null>(null) const [syncOptionsLocked, setSyncOptionsLocked] = useState(false) const [syncOptions, setSyncOptions] = useState>({}) @@ -41,9 +49,25 @@ export default function Sync() { const [filterOptions, setFilterOptions] = useState>({}) const [filterOptionsJson, setFilterOptionsJson] = useState('{}') + const [configOptionsLocked, setConfigOptionsLocked] = useState(false) + const [configOptions, setConfigOptions] = useState>({}) + const [configOptionsJson, setConfigOptionsJson] = useState('{}') + const [cronExpression, setCronExpression] = useState(null) - const [globalOptions, setGlobalOptions] = useState([]) + const [currentGlobalOptions, setCurrentGlobalOptions] = useState([]) + + useEffect(() => { + setConfigOptionsJson(JSON.stringify(RCLONE_CONFIG_DEFAULTS, null, 2)) + + return () => { + setConfigOptionsJson('{}') + } + }, []) + + useEffect(() => { + getCurrentGlobalFlags().then((flags) => setCurrentGlobalOptions(flags)) + }, []) // biome-ignore lint/correctness/useExhaustiveDependencies: when unlocking, we don't want to re-run the effect useEffect(() => { @@ -54,6 +78,7 @@ export default function Sync() { let mergedSyncDefaults = {} let mergedFilterDefaults = {} + let mergedConfigDefaults = {} // Helper function to merge defaults from a remote const mergeRemoteDefaults = (remote: string | null) => { @@ -74,6 +99,13 @@ export default function Sync() { ...remoteConfig.filterDefaults, } } + + if (remoteConfig.configDefaults) { + mergedConfigDefaults = { + ...mergedConfigDefaults, + ...remoteConfig.configDefaults, + } + } } // Only merge defaults for remote paths @@ -87,26 +119,29 @@ export default function Sync() { if (Object.keys(mergedFilterDefaults).length > 0 && !filterOptionsLocked) { setFilterOptionsJson(JSON.stringify(mergedFilterDefaults, null, 2)) } + + if (Object.keys(mergedConfigDefaults).length > 0 && !configOptionsLocked) { + setConfigOptionsJson(JSON.stringify(mergedConfigDefaults, null, 2)) + } }, [source, dest]) useEffect(() => { - getGlobalFlags().then((flags) => setGlobalOptions(flags)) - }, []) - - useEffect(() => { - let step: 'sync' | 'filter' = 'sync' + let step: 'sync' | 'filter' | 'config' = 'sync' try { setSyncOptions(JSON.parse(syncOptionsJson)) step = 'filter' setFilterOptions(JSON.parse(filterOptionsJson)) + step = 'config' + setConfigOptions(JSON.parse(configOptionsJson)) + setJsonError(null) } catch (error) { setJsonError(step) console.error(`Error parsing ${step} options:`, error) } - }, [syncOptionsJson, filterOptionsJson]) + }, [syncOptionsJson, filterOptionsJson, configOptionsJson]) const handleStartSync = useCallback(async () => { setIsLoading(true) @@ -155,6 +190,11 @@ export default function Sync() { return } + const mergedConfig = { + ...configOptions, + ...syncOptions, + } + if (cronExpression) { try { cronstrue.toString(cronExpression) @@ -167,13 +207,13 @@ export default function Sync() { return } usePersistedStore.getState().addScheduledTask({ - 'type': 'sync', - 'cron': cronExpression, - 'args': { - 'source': source, - 'dest': dest, - 'syncOptions': syncOptions, - 'filterOptions': filterOptions, + type: 'sync', + cron: cronExpression, + args: { + source: source, + dest: dest, + syncOptions: mergedConfig, + filterOptions: filterOptions, }, }) } @@ -182,7 +222,7 @@ export default function Sync() { await startSync({ srcFs: source, dstFs: dest, - _config: syncOptions, + _config: mergedConfig, _filter: filterOptions, }) @@ -201,7 +241,7 @@ export default function Sync() { } finally { setIsLoading(false) } - }, [source, dest, syncOptions, filterOptions, cronExpression]) + }, [source, dest, syncOptions, filterOptions, cronExpression, configOptions]) const buttonText = useMemo(() => { if (isLoading) return 'STARTING...' @@ -244,8 +284,10 @@ export default function Sync() { + } /> + } + indicator={} + subtitle="Tap to toggle config options for this operation" + title="Config" + > + +