import * as Sentry from '@sentry/browser' import { getVersion as getUiVersion } from '@tauri-apps/api/app' import { Channel, invoke } from '@tauri-apps/api/core' import { getCurrentWindow } from '@tauri-apps/api/window' import { writeText } from '@tauri-apps/plugin-clipboard-manager' import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link' import { ask, message } from '@tauri-apps/plugin-dialog' import { debug, error, info, trace, warn } from '@tauri-apps/plugin-log' import { platform } from '@tauri-apps/plugin-os' import { exit, relaunch } from '@tauri-apps/plugin-process' import { type Update, check } from '@tauri-apps/plugin-updater' import { CronExpressionParser } from 'cron-parser' import { defaultOptions } from 'tauri-plugin-sentry-api' import { getDeepLinkUrl, handleDeepLinkUrl } from './lib/deep' import { CLOSE_APP, RELAUNCH_APP, RESTART_RCLONE, type RestartRclonePayload } from './lib/events' import { LOCAL_HOST_ID, RC_PORT, getHostInfo, makeLocalHost } from './lib/hosts' import { validateLicense } from './lib/license' import notify from './lib/notify' import queryClient from './lib/query' import { listTransfers, startBisync, startCopy, startDelete, startMount, startMove, startPurge, startSync, } from './lib/rclone/api' import rcloneClient from './lib/rclone/client' import { compareVersions } from './lib/rclone/common' import { initRclone } from './lib/rclone/init' import { initTray } from './lib/tray' import { openSmallWindow } from './lib/window' import { initHostStore, useHostStore } from './store/host' import { waitForStoreHydration } from './store/lib' import { useStore } from './store/memory' import { selectCurrentHost, usePersistedStore } from './store/persisted' import type { ScheduledTask } from './types/schedules' let rcloneListenersRegistered = false // Mirrors zookeeper.rs RcloneEvent — only 'close' is emitted. type RcloneDaemonEvent = { kind: 'close' code: number | null intentional: boolean } async function killRcloneDaemon() { // Rust daemon state is authoritative: the command no-ops (returns false) when nothing is // tracked, so we must not gate on a local mirror that could defeat its reload-orphan guard. let killed = false try { killed = await invoke('kill_rclone_daemon', {}) } catch (error) { console.error('[killRcloneDaemon] failed to kill rclone daemon', error) Sentry.captureException(error) } if (killed) { await new Promise((resolve) => setTimeout(resolve, 1000)) } } try { Sentry.init({ ...defaultOptions, sendDefaultPii: false, }) } catch { console.error('Error initializing Sentry') } // forward console logs in webviews to the tauri logger, so they show up in terminal function forwardConsole( fnName: 'log' | 'debug' | 'info' | 'warn' | 'error', logger: (message: string) => Promise ) { try { const original = console[fnName] console[fnName] = (message, ...args) => { original(message, ...args) logger( `${message} ${args?.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')}` ) } } catch {} } try { forwardConsole('log', trace) forwardConsole('debug', debug) forwardConsole('info', info) forwardConsole('warn', warn) forwardConsole('error', error) } catch (error) { console.error('Could not enable console logs', error) } async function checkFlatpakPermissions() { const hasPermissions = await invoke('has_flatpak_permissions') if (hasPermissions) return const copyCommand = await ask( 'You are running the flatpak version of Rclone UI, which is sandboxed.\n\nrclone is a file management utility that needs disk access in order to run. Please allow it using the following command (copy paste in your terminal):\n\nflatpak override --user --filesystem=host com.rcloneui.RcloneUI\n\nRestart Rclone UI afterwards.', { title: 'Flatpak Permissions Required', kind: 'warning', okLabel: 'Copy Command & Exit', cancelLabel: 'Exit', } ) if (copyCommand) { await writeText('flatpak override --user --filesystem=host com.rcloneui.RcloneUI') } await exit() } async function waitForHydration() { console.log('[waitForHydration] waiting for store hydration') await waitForStoreHydration(() => usePersistedStore.persist.hasHydrated()) console.log('[waitForHydration] store hydrated') } async function initializeHostStore() { console.log('[initializeHostStore] initializing') // Default to 'local' if fresh install/no host selected const hostId = usePersistedStore.getState().currentHostId || LOCAL_HOST_ID await initHostStore(hostId) console.log('[initializeHostStore] initialized for', hostId) } async function checkHostReachability(): Promise { console.log('[checkHostReachability] checking host reachability') const currentHost = selectCurrentHost(usePersistedStore.getState()) // If no host selected or local host, skip check (local rclone hasn't started yet) if (!currentHost || currentHost.id === LOCAL_HOST_ID) { console.log('[checkHostReachability] local or no host, skipping') return } console.log('[checkHostReachability] checking remote host:', currentHost.name) const checkReachability = async (): Promise => { try { const hostInfo = await getHostInfo({ url: currentHost.url, authUser: currentHost.authUser, authPassword: currentHost.authPassword, }) return hostInfo !== null } catch (err) { console.error('[checkHostReachability] error:', err) return false } } let isReachable = await checkReachability() while (!isReachable) { console.log('[checkHostReachability] host not reachable') const answer = await ask( `The selected host "${currentHost.name}" is not reachable.\n\nURL: ${currentHost.url}\n\nWould you like to retry, switch to local host, or exit?`, { title: 'Host Not Reachable', kind: 'warning', okLabel: 'Retry', cancelLabel: 'Use Local', } ) if (answer) { // User chose to retry console.log('[checkHostReachability] retrying connection') isReachable = await checkReachability() } else { // User chose to use local host. Upsert the local host and point at it in one write so // currentHostId never dangles. console.log('[checkHostReachability] switching to local host') usePersistedStore.setState((prev) => ({ hosts: prev.hosts.some((h) => h.id === LOCAL_HOST_ID) ? prev.hosts : [...prev.hosts, makeLocalHost()], currentHostId: LOCAL_HOST_ID, })) // Re-initialize host store for local await initHostStore(LOCAL_HOST_ID) return } } console.log('[checkHostReachability] host is reachable') } async function validateInstance() { console.log('[validateInstance] validating license') const licenseKey = usePersistedStore.getState().licenseKey if (!licenseKey) { console.log('[validateInstance] no license key, skipping license validation') usePersistedStore.setState({ licenseValid: false }) return } if (!navigator.onLine) { console.log('[validateInstance] not online, skipping license validation') return } try { await validateLicense(licenseKey) } catch (e) { console.log('[validateInstance] error validating license, marking as invalid') usePersistedStore.setState({ licenseValid: false }) if (e instanceof Error) { await message(e.message, { title: 'Error Validating License', kind: 'error', okLabel: 'OK', }) console.log('[validateInstance] error message displayed, returning') return } await message('An error occurred while validating your license. Please try again.', { title: 'Error', kind: 'error', okLabel: 'OK', }) console.log('[validateInstance] default error message displayed, returning') } finally { console.log('[validateInstance] license validation complete') } } async function checkAlreadyRunning() { console.log('[checkAlreadyRunning]') try { const running = await invoke('is_rclone_running', { port: RC_PORT }) console.log('[checkAlreadyRunning] running', running) if (running) { const confirmed = await ask( 'Rclone is already running on this system.\n\nPlease stop it before launching Rclone UI.', { title: 'Rclone Already Running', kind: 'info', okLabel: 'Close Rclone', cancelLabel: 'Exit UI', } ) console.log('[checkAlreadyRunning] confirmed', confirmed) if (confirmed) { console.log('[checkAlreadyRunning] closing rclone') if (platform() === 'windows') { console.log('[checkAlreadyRunning] windows, showing message') await message( "If you're on Windows, you might notice a few powershell/terminal dialogs open and close.\n\nThis is normal and expected, imagine we are playing whack-a-mole with the rclone process to close it.", { 'title': 'Trigger Warning', 'kind': 'info', 'okLabel': 'Got it', } ) } const result = await invoke('stop_rclone_processes') console.log('[checkAlreadyRunning] stop_rclone_processes', result) await new Promise((resolve) => setTimeout(resolve, 700)) } else { console.log('[checkAlreadyRunning] exiting') await exit(0) } } } catch (err) { console.error('[checkAlreadyRunning] error', err) Sentry.captureException(err) } } async function registerRcloneWindowListeners() { if (rcloneListenersRegistered) { return } const window = getCurrentWindow() // Kill the daemon BEFORE exit/relaunch — this ordering is the entire point of these listeners. const shutdown = async (mode: 'quit' | 'relaunch') => { // A dead daemon means "no active transfers": don't let a listTransfers throw make quit a // silent no-op. const transfers = await queryClient .ensureQueryData({ queryKey: ['transfers', 'list', 'all'], queryFn: async () => await listTransfers(), staleTime: 10_000, // 10 seconds gcTime: 60_000, // 1 minute }) .catch(() => null) if (transfers?.active && transfers.active.length > 0) { const answer = await ask('All active transfers will be stopped.', { title: 'Exit', kind: 'info', okLabel: mode === 'relaunch' ? 'Relaunch' : 'Quit', cancelLabel: 'Cancel', }) if (!answer) { return } } const cloudflaredTunnel = useStore.getState().cloudflaredTunnel if (cloudflaredTunnel) { try { console.log('[shutdown] stopping cloudflared tunnel') await invoke('stop_cloudflared_tunnel', { pid: cloudflaredTunnel.pid }) useStore.setState({ cloudflaredTunnel: null }) } catch (error) { console.error('[shutdown] failed to stop cloudflared tunnel', error) } } await killRcloneDaemon() if (mode === 'relaunch') { await relaunch() } else { await exit(0) } } await window.listen(CLOSE_APP, async () => { console.log('[registerRcloneWindowListeners] close-app requested') await shutdown('quit') }) console.log('[registerRcloneWindowListeners] close-app listener registered') await window.listen(RELAUNCH_APP, async () => { console.log('[registerRcloneWindowListeners] relaunch-app requested') await shutdown('relaunch') }) console.log('[registerRcloneWindowListeners] relaunch-app listener registered') await window.listen(RESTART_RCLONE, async (event) => { console.log('[registerRcloneWindowListeners] restart-rclone requested') // Trust the payload: the initiating webview's store writes may not have reached the main // window yet. Apply BEFORE the in-flight guard so state isn't lost on a skipped restart. // configFiles BEFORE activeConfigId (setActiveConfigFile resolves against state.configFiles // and nulls on a miss). NEVER log the raw payload — it carries config `pass`. const payload = event.payload if (payload) { if (payload.rclonePath) { usePersistedStore.getState().setRclonePath(payload.rclonePath) } if (payload.defaultConfigPath) { useHostStore.getState().setDefaultConfigPath(payload.defaultConfigPath) } if (payload.configFiles) { useHostStore.setState({ configFiles: payload.configFiles }) } if (payload.activeConfigId) { useHostStore.getState().setActiveConfigFile(payload.activeConfigId) } if (payload.proxy !== undefined) { useHostStore.setState({ proxy: payload.proxy }) } } if (useStore.getState().isRestartingRclone) { console.log('[restart-rclone] restart already in progress, ignoring request') return } useStore.setState({ isRestartingRclone: true }) try { await killRcloneDaemon() await startRclone() } catch (error) { console.error('[restart-rclone] failed to restart rclone', error) Sentry.captureException(error) } finally { useStore.setState({ isRestartingRclone: false }) } }) console.log('[registerRcloneWindowListeners] restart-rclone listener registered') rcloneListenersRegistered = true } async function startRclone() { console.log('[startRclone]') await registerRcloneWindowListeners() let rclone: Awaited> | null = null try { rclone = await initRclone([ 'rcd', // ...(platform() === 'macos' // ? ['--rc-no-auth'] // webkit doesn't allow for credentials in the url // : ['--rc-user', 'admin', '--rc-pass', sessionPassword]), '--rc-no-auth', '--rc-serve', '--rc-job-expire-duration', '24h', '--rc-job-expire-interval', '1h', // defaults // '-rc-addr', // ':5572', ]) } catch (error) { Sentry.captureException(error) await message( error instanceof Error ? error.message : 'Failed to start rclone, please try again later.', { title: 'Error', kind: 'error', okLabel: 'Exit', } ) return await exit(0) } if (!rclone) { console.error('[startRclone] initRclone returned without a runnable command') Sentry.captureException(new Error('initRclone returned without a runnable command.')) return } const { path, args: rcloneArgs, env } = rclone const channel = new Channel() channel.onmessage = async (payload) => { console.log('[startRclone] daemon close', payload) // Killed intentionally (restart / quit) — the initiator handles what happens next. if (payload.intentional) { return } if (platform() === 'windows') { return await exit(0) } if (payload.code === 143 || payload.code === 1) { Sentry.captureException(new Error('Rclone has crashed')) const confirmed = await ask('Rclone has crashed', { title: 'Error', kind: 'error', okLabel: 'Relaunch', cancelLabel: 'Exit', }) if (!confirmed) { return await exit(0) } await relaunch() } } console.log('[startRclone] spawning rclone daemon') let pid: number try { pid = await invoke('spawn_rclone', { path, args: rcloneArgs, env, onEvent: channel, }) } catch (error) { console.error('[startRclone] failed to spawn rclone daemon', error) Sentry.captureException(error) // A relaunch re-runs the resolution ladder, which can heal a broken binary. const confirmed = await ask( `Rclone failed to start: ${error instanceof Error ? error.message : String(error)}`, { title: 'Error', kind: 'error', okLabel: 'Relaunch', cancelLabel: 'Exit', } ) if (confirmed) { await relaunch() return } return await exit(0) } console.log('[startRclone] running rclone, pid', pid) await new Promise((resolve) => setTimeout(resolve, 500)) } async function startupMounts() { console.log('[startupMounts]') const remoteConfigList = useHostStore.getState().remoteConfigs const remotes = await queryClient.ensureQueryData({ queryKey: ['remotes', 'list', 'all'], queryFn: async () => await rcloneClient('/config/listremotes').then((r) => r?.remotes), staleTime: 1000 * 60, }) console.log('[startupMounts] remotes', remotes) for (const remote of remotes) { console.log('[startupMounts] remote', remote) const remoteConfig = remoteConfigList[remote] if (!remoteConfig) { console.log('[startupMounts] remote config not found', remote) continue } console.log('[startupMounts] remote config found', remoteConfig) if (remoteConfig.mountOnStart?.enabled && remoteConfig.mountOnStart?.mountPoint) { console.log( '[startupMounts] remote config mount on start enabled', remoteConfig.mountOnStart ) try { const { mountPoint, remotePath, mountOptions, vfsOptions, filterOptions, configOptions, } = remoteConfig.mountOnStart console.log('[startupMounts] starting mount', { source: `${remote}:${remotePath}`, destination: mountPoint, options: { mount: mountOptions, vfs: vfsOptions, filter: filterOptions, config: configOptions, }, }) console.log('[startupMounts] starting mount') await startMount({ source: `${remote}:${remotePath}`, destination: mountPoint, options: { mount: mountOptions, vfs: vfsOptions, filter: filterOptions, config: configOptions, }, }) console.log('[startupMounts] mount started') } catch (error) { console.error('Error mounting remote:', error) Sentry.captureException(error) await message( error instanceof Error ? error.message : `Failed to mount ${remote} on startup.`, { title: 'Automount Error', kind: 'error', okLabel: 'Got it', } ) } } } } async function showStartup() { console.log('[showStartup] showing startup') const hideStartup = usePersistedStore.getState().hideStartup if (hideStartup) { console.log('[showStartup] startup hidden, returning') return } const startupDisplayed = useStore.getState().startupDisplayed if (startupDisplayed) { console.log('[showStartup] startup already displayed, returning') return } console.log('[showStartup] startup not displayed, setting displayed and status') // Upgrade-only: a successful auto-update's 'updated' status must survive so its message shows; // everything else (normal launch with null status, or a failed update restored to // 'initializing') becomes 'initialized'. Never unconditionally clobber, or 'updated' is lost. const currentStartupStatus = useStore.getState().startupStatus useStore.setState({ startupDisplayed: true, startupStatus: currentStartupStatus === 'updated' ? 'updated' : 'initialized', }) console.log('[showStartup] store updated with startup displayed and status set') await openSmallWindow({ name: 'Startup', url: '/startup', }) console.log('[showStartup] startup window opened') if (!['windows', 'macos'].includes(platform())) { usePersistedStore.setState({ hideStartup: true }) } console.log('[showStartup] startup hidden') } const MAX_INT_MS = 2_147_483_647 let hasScheduledTasks = false async function resumeTasks() { console.log('[resumeTasks] resuming tasks') if (hasScheduledTasks) { console.log('[resumeTasks] already called, skipping (hasScheduledTasks=true)') return } const scheduledTasks = useHostStore.getState().scheduledTasks const activeConfigId = useHostStore.getState().activeConfigId console.log('[resumeTasks] found', scheduledTasks.length, 'scheduled tasks') console.log('[resumeTasks] activeConfigId:', activeConfigId) if (!activeConfigId) { console.log('[resumeTasks] no active config id, cannot schedule tasks') return } hasScheduledTasks = true console.log('[resumeTasks] processing tasks for config:', activeConfigId) let scheduledCount = 0 let skippedRunning = 0 let skippedConfigMismatch = 0 let skippedTimingIssue = 0 for (const task of scheduledTasks) { console.log('[resumeTasks] processing task:', { id: task.id, operation: task.operation, cron: task.cron, configId: task.configId, isRunning: task.isRunning, isEnabled: task.isEnabled, }) if (!task.isEnabled) { console.log('[resumeTasks] task', task.id, 'is disabled, skipping') continue } if (task.isRunning) { console.log('[resumeTasks] task', task.id, 'was marked as running, resetting state') useHostStore.getState().updateScheduledTask(task.id, { isRunning: false, currentRunId: undefined, lastRunError: 'Task closed prematurely', }) skippedRunning++ continue } if (task.configId !== activeConfigId) { console.log( '[resumeTasks] task', task.id, 'belongs to different config:', task.configId, '!==', activeConfigId ) skippedConfigMismatch++ continue } try { console.log('[resumeTasks] parsing cron expression:', task.cron) const cronInterval = CronExpressionParser.parse(task.cron) const nextRun = cronInterval.next().toDate() const now = Date.now() const difference = nextRun.getTime() - now console.log('[resumeTasks] task', task.id, 'timing:', { nextRun: nextRun.toISOString(), now: new Date(now).toISOString(), differenceMs: difference, differenceMinutes: Math.round(difference / 60000), maxAllowedMs: MAX_INT_MS, withinLimit: difference <= MAX_INT_MS, isPositive: difference > 0, }) if (difference <= MAX_INT_MS && difference > 0) { console.log( '[resumeTasks] scheduling task', task.id, 'to run in', Math.round(difference / 60000), 'minutes' ) setTimeout(async () => { console.log( '[resumeTasks] timer fired for task', task.id, 'at', new Date().toISOString() ) notify({ title: 'Task Started', body: `Task ${task.operation} (${task.id}) started`, }) await handleTask(task) }, difference) scheduledCount++ console.log( '[resumeTasks] task', task.id, 'scheduled successfully for', nextRun.toISOString() ) } else { console.log( '[resumeTasks] task', task.id, 'NOT scheduled:', difference > MAX_INT_MS ? 'next run too far in future' : 'next run is in the past or now' ) skippedTimingIssue++ } } catch (error) { console.error('[resumeTasks] error scheduling task', task.id, ':', error) console.error('[resumeTasks] task details:', JSON.stringify(task, null, 2)) Sentry.captureException(error) } } console.log('[resumeTasks] summary:', { totalTasks: scheduledTasks.length, scheduledCount, skippedRunning, skippedConfigMismatch, skippedTimingIssue, }) } async function handleTask(task: ScheduledTask) { console.log('[handleTask] starting execution for task:', task.id, task.operation) const currentTask = useHostStore.getState().scheduledTasks.find((t) => t.id === task.id) if (!currentTask) { console.log('[handleTask] task', task.id, 'not found in store, aborting') return } console.log('[handleTask] found task in store:', { id: currentTask.id, isRunning: currentTask.isRunning, currentRunId: currentTask.currentRunId, isEnabled: currentTask.isEnabled, }) if (currentTask.isRunning) { console.log( '[handleTask] task', task.id, 'already running (runId:', currentTask.currentRunId, '), aborting' ) return } const freshRunId = crypto.randomUUID() console.log('[handleTask] generated freshRunId:', freshRunId) useHostStore.getState().updateScheduledTask(task.id, { isRunning: true, currentRunId: freshRunId, lastRun: new Date().toISOString(), }) console.log('[handleTask] running task', task.operation, task.id) console.log('[handleTask] updated task state, isRunning=true, runId:', freshRunId) const currentRunId = useHostStore .getState() .scheduledTasks.find((t) => t.id === task.id)?.currentRunId console.log('[handleTask] verifying runId - expected:', freshRunId, 'actual:', currentRunId) if (currentRunId !== freshRunId) { console.log('[handleTask] runId mismatch, another execution may have started, aborting') return } console.log( '[handleTask] executing operation:', task.operation, 'with args:', JSON.stringify(task.args, null, 2) ) try { switch (task.operation) { case 'copy': { console.log('[handleTask] starting copy operation') const { sources, options, destination } = task.args await startCopy({ sources, destination, options, }) console.log('[handleTask] copy operation completed') break } case 'move': { console.log('[handleTask] starting move operation') const { sources, options, destination } = task.args await startMove({ sources, destination, options, }) console.log('[handleTask] move operation completed') break } case 'sync': { console.log('[handleTask] starting sync operation') const { source, destination, options } = task.args await startSync({ source, destination, options, }) console.log('[handleTask] sync operation completed') break } case 'bisync': { console.log('[handleTask] starting bisync operation') const { source, destination, options } = task.args await startBisync({ source, destination, options, }) console.log('[handleTask] bisync operation completed') break } case 'delete': { console.log('[handleTask] starting delete operation') const { sources, options } = task.args await startDelete({ sources, options, }) console.log('[handleTask] delete operation completed') break } case 'purge': { console.log('[handleTask] starting purge operation') const { sources, options } = task.args await startPurge({ sources, options, }) console.log('[handleTask] purge operation completed') break } default: console.log('[handleTask] unknown operation encountered') break } console.log('[handleTask] task', task.id, 'completed successfully') } catch (err) { Sentry.captureException(err) console.error('[handleTask] task', task.id, 'failed with error:', err) console.error('[handleTask] task args were:', JSON.stringify(task.args, null, 2)) useHostStore.getState().updateScheduledTask(task.id, { lastRunError: err instanceof Error ? err.message : 'Unknown error', }) } finally { console.log('[handleTask] cleaning up task', task.id, 'state') useHostStore.getState().updateScheduledTask(task.id, { isRunning: false, currentRunId: undefined, }) } } async function installUpdate(update: Update, required: boolean) { const confirmed = await ask( 'You are running an outdated version of Rclone UI. Please update to the latest version.', { title: required ? 'Update Required' : 'Update Available', kind: 'info', okLabel: 'Update', cancelLabel: required ? 'Exit' : 'Cancel', } ) if (!confirmed) { console.log('[checkVersion] user cancelled update') if (required) { return await exit(0) } return } console.log('[checkVersion] downloading and installing update') await update.downloadAndInstall() console.log('[checkVersion] update downloaded and installed') await message('Rclone UI has been updated. Please restart the application.', { title: 'Update Complete', kind: 'info', okLabel: 'Restart', }) console.log('[checkVersion] relaunching app') // Direct relaunch: at checkVersion time no daemon exists yet (before startRclone), so the // shutdown path is unnecessary; the old emit fired before any listener existed and was // dropped, leaving the app running the old version. await relaunch() } async function checkVersion() { console.log('[checkVersion]') try { console.log('[checkVersion] fetching meta.json') const latestMeta = await fetch('https://rcloneui.com/latest') console.log('[checkVersion] meta.json fetched') const latestMetaData = (await latestMeta.json()) as { minimumVersion: string okVersion: string } console.log('[checkVersion] meta.json parsed') const { minimumVersion, okVersion } = latestMetaData console.log('[checkVersion] minimumVersion', minimumVersion) console.log('[checkVersion] okVersion', okVersion) const currentVersion = await getUiVersion() console.log('[checkVersion] currentVersion', currentVersion) if ( compareVersions(currentVersion, minimumVersion) >= 0 && compareVersions(currentVersion, okVersion) >= 0 ) { console.log('[checkVersion] currentVersion is up to date') return } console.log('[checkVersion] checking for update') const receivedUpdate = await check({ allowDowngrades: true, timeout: 30000, }) console.log('[checkVersion] update check complete') if (!receivedUpdate) { console.log('[checkVersion] no update found') return } if (compareVersions(currentVersion, minimumVersion) < 0) { console.log('[checkVersion] currentVersion is outdated') await installUpdate(receivedUpdate, true) } else if (compareVersions(currentVersion, okVersion) < 0) { console.log('[checkVersion] checking for update') await installUpdate(receivedUpdate, false) } } catch (error) { console.error('[checkVersion] error', error) Sentry.captureException(error) } } async function checkRclone() { let currentHost = selectCurrentHost(usePersistedStore.getState()) ?? makeLocalHost() let hostInfo = await getHostInfo({ url: currentHost.url, authUser: currentHost.authUser, authPassword: currentHost.authPassword, }) if (!hostInfo) { await message( 'Failed to get host info, is it online and reachable?\n\nSwitching to local host.', { title: 'Host Not Reachable', kind: 'error', } ) currentHost = makeLocalHost() hostInfo = await getHostInfo({ url: currentHost.url, authUser: currentHost.authUser, authPassword: currentHost.authPassword, }) if (!hostInfo) { const confirmed = await ask( 'Local host is not reachable, please try again or file an issue on Github if the problem persists.', { title: 'Local Host Not Reachable', kind: 'error', okLabel: 'Exit', } ) if (confirmed) { return await exit(0) } await new Promise((resolve) => setTimeout(resolve, 60_000)) return await exit(0) } } currentHost = { ...currentHost, os: hostInfo.os, cliVersion: hostInfo.cliVersion, } console.log('[checkRclone] setting currentHost', currentHost) usePersistedStore.setState((prev) => ({ hosts: [...prev.hosts.filter((h) => h.id !== currentHost.id), currentHost], currentHostId: currentHost.id, })) } getCurrentWindow().listen('tauri://close-requested', async () => { console.log('(main) window close requested') await getCurrentWindow().destroy() }) function processDeepLink(url: string) { const deepLinkUrl = getDeepLinkUrl(url) console.log('deep link url', deepLinkUrl) handleDeepLinkUrl(deepLinkUrl) useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' }) } onOpenUrl((urls) => { console.log('deep links while running', urls) processDeepLink(urls[0]) }) async function handleDeepLink() { console.log('[handleDeepLink] getting current deep links') const urls = await getCurrent() if (!urls || urls.length === 0) { console.log('[handleDeepLink] no deep links found') return } processDeepLink(urls[0]) } waitForHydration() .then(() => checkFlatpakPermissions()) .then(() => initializeHostStore()) .then(() => checkHostReachability()) .then(() => registerRcloneWindowListeners()) .then(() => checkVersion()) .then(() => validateInstance()) .then(() => checkAlreadyRunning()) .then(() => startRclone()) .then(() => checkRclone()) .then(() => handleDeepLink()) .then(() => showStartup()) .then(() => startupMounts()) .then(() => resumeTasks()) .then(() => initTray()) .catch(console.error)