149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
||
import { useEffect, useState } from 'react'
|
||
import type { RcloneFeatures, RcloneFsInfo } from '../types/rclone'
|
||
import { UserCancelledError } from './errors'
|
||
import { sortByName } from './flags'
|
||
import rclone from './rclone/client'
|
||
import { SERVE_TYPES } from './rclone/constants'
|
||
|
||
// Wall-clock tick for values derived from "now" (relative timestamps, next cron occurrences).
|
||
// Memoizing such values without a time dep freezes them at their last dep change. Pass null to
|
||
// pause (e.g. while a drawer is closed); re-arming refreshes immediately.
|
||
export function useNow(intervalMs: number | null = 30_000): number {
|
||
const [now, setNow] = useState(() => Date.now())
|
||
|
||
useEffect(() => {
|
||
if (intervalMs === null) {
|
||
return
|
||
}
|
||
setNow(Date.now())
|
||
const id = setInterval(() => setNow(Date.now()), intervalMs)
|
||
return () => clearInterval(id)
|
||
}, [intervalMs])
|
||
|
||
return now
|
||
}
|
||
|
||
// Shared query options for a remote's `/config/get`. No default staleTime: most consumers rely on
|
||
// staleTime-0 refetch-on-mount for cross-window freshness (each webview has its own QueryClient);
|
||
// the handful that want caching spread `staleTime` per-site.
|
||
export function remoteConfigQueryOptions(remote: string | undefined | null) {
|
||
return {
|
||
queryKey: ['remote', remote, 'config'] as const,
|
||
queryFn: () => rclone('/config/get', { params: { query: { name: remote! } } }),
|
||
enabled: !!remote && remote !== 'UI_LOCAL_FS' && remote !== 'UI_FAVORITES',
|
||
}
|
||
}
|
||
|
||
export function useRemoteConfig(remote: string | undefined | null) {
|
||
return useQuery(remoteConfigQueryOptions(remote))
|
||
}
|
||
|
||
// Shared query options for a remote's `operations/fsinfo` — the authoritative, per-remote backend
|
||
// capability set (correct even for wrapping backends like crypt/alias/union, which the old static
|
||
// type lists could not express). fsinfo instantiates/connects the remote, so it's cached hard:
|
||
// capabilities are ~immutable per config and only change on a remote edit (explicit invalidation)
|
||
// or an rclone upgrade (self-heals on the 24h staleTime / next cold launch). retry: 1 overrides the
|
||
// app-wide 3-retry default so an unreachable remote costs one attempt, then error-caches.
|
||
export function fsInfoQueryOptions(remote: string | undefined | null) {
|
||
return {
|
||
queryKey: ['remote', remote, 'fsinfo'] as const,
|
||
queryFn: () => rclone('/operations/fsinfo', { params: { query: { fs: `${remote}:` } } }),
|
||
enabled: !!remote && remote !== 'UI_LOCAL_FS' && remote !== 'UI_FAVORITES',
|
||
staleTime: 1000 * 60 * 60 * 24,
|
||
// Cap retries low (dead remotes shouldn't retry 3× like the default) but keep the default's
|
||
// UserCancelledError guard: a plain `retry: 1` would override it, and since fsinfo connects
|
||
// to the remote, retrying a dismissed reconnect prompt re-prompts the user (client.ts).
|
||
retry: (failureCount: number, error: unknown) =>
|
||
!(error instanceof UserCancelledError) && failureCount < 1,
|
||
}
|
||
}
|
||
|
||
export function useFsInfo(remote: string | undefined | null) {
|
||
return useQuery(fsInfoQueryOptions(remote))
|
||
}
|
||
|
||
// undefined while loading/errored → false. Callers that need an optimistic default (e.g. create
|
||
// folder, which historically assumed "supported" until proven otherwise) handle that explicitly.
|
||
export function hasFeature(
|
||
fsInfo: RcloneFsInfo | undefined,
|
||
feature: keyof RcloneFeatures
|
||
): boolean {
|
||
return !!fsInfo?.Features?.[feature]
|
||
}
|
||
|
||
export function useFlags() {
|
||
const allFlagsQuery = useQuery({
|
||
queryKey: ['options', 'all'],
|
||
queryFn: async () => await rclone('/options/info'),
|
||
})
|
||
|
||
const globalFlagsQuery = useQuery({
|
||
queryKey: ['options', 'global'],
|
||
queryFn: async () => await rclone('/options/get'),
|
||
})
|
||
|
||
const globalFlags = globalFlagsQuery.data
|
||
const allFlags = allFlagsQuery.data
|
||
|
||
const filterFlags = allFlags?.filter
|
||
?.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||
.sort(sortByName)
|
||
|
||
const configFlags = allFlags?.main
|
||
?.filter(
|
||
(flag) =>
|
||
flag.Groups?.includes('Performance') ||
|
||
flag.Groups?.includes('Listing') ||
|
||
flag.Groups?.includes('Networking') ||
|
||
flag.Groups?.includes('Check') ||
|
||
flag.Name === 'use_server_modtime'
|
||
)
|
||
.sort(sortByName)
|
||
|
||
const mountFlags = allFlags?.mount
|
||
?.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||
.sort(sortByName)
|
||
|
||
const vfsFlags = allFlags?.vfs
|
||
?.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||
.sort(sortByName)
|
||
|
||
const copyFlags = allFlags?.main
|
||
?.filter((flag) => flag.Groups?.includes('Copy'))
|
||
.sort(sortByName)
|
||
|
||
const syncFlags = allFlags?.main
|
||
?.filter((flag) => flag.Groups?.includes('Copy') || flag.Groups?.includes('Sync'))
|
||
.sort(sortByName)
|
||
|
||
const serveFlags = SERVE_TYPES.reduce(
|
||
(acc, type) => {
|
||
acc[type] = (allFlags?.[type] || [])
|
||
.map((flag: any) => ({
|
||
...flag,
|
||
FieldName: flag.Name,
|
||
DefaultStr:
|
||
flag.Name === 'addr'
|
||
? flag.DefaultStr.replace('[', '').replace(']', '')
|
||
: flag.DefaultStr,
|
||
}))
|
||
.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
|
||
return acc
|
||
},
|
||
{} as Record<(typeof SERVE_TYPES)[number], any[]>
|
||
)
|
||
|
||
return {
|
||
allFlags,
|
||
globalFlags,
|
||
filterFlags,
|
||
configFlags,
|
||
mountFlags,
|
||
vfsFlags,
|
||
copyFlags,
|
||
syncFlags,
|
||
serveFlags,
|
||
}
|
||
}
|