remote capability check
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { RcloneFeatures, RcloneFsInfo } from '../types/rclone'
|
||||
import { sortByName } from './flags'
|
||||
import rclone from './rclone/client'
|
||||
import { SERVE_TYPES } from './rclone/constants'
|
||||
@@ -37,6 +38,36 @@ 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,
|
||||
retry: 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'],
|
||||
|
||||
+4
-139
@@ -28,142 +28,7 @@ export const RCLONE_RELEASES_SHOWN = 20
|
||||
|
||||
export const SERVE_TYPES = ['dlna', 'ftp', 'sftp', 'http', 'nfs', 'restic', 's3', 'webdav'] as const
|
||||
|
||||
export const SUPPORTS_CLEANUP = [
|
||||
's3',
|
||||
'b2',
|
||||
'box',
|
||||
'filefabric',
|
||||
'filen',
|
||||
'drive',
|
||||
'internetarchive',
|
||||
'jottacloud',
|
||||
'mailru',
|
||||
'mega',
|
||||
'onedrive',
|
||||
'oos',
|
||||
'pcloud',
|
||||
'pikpak',
|
||||
'putio',
|
||||
'protondrive',
|
||||
'qingstor',
|
||||
'seafile',
|
||||
'yandex',
|
||||
] as const
|
||||
|
||||
export const SUPPORTS_PURGE = [
|
||||
'netstorage',
|
||||
'box',
|
||||
'sharefile',
|
||||
'dropbox',
|
||||
'filefabric',
|
||||
'filescom',
|
||||
'filen',
|
||||
'gofile',
|
||||
'gcs',
|
||||
'drive',
|
||||
'hdfs',
|
||||
'hifile',
|
||||
'iclouddrive',
|
||||
'imagekit',
|
||||
'jottacloud',
|
||||
'koofr',
|
||||
'mailru',
|
||||
'mega',
|
||||
'azureblob',
|
||||
'onedrive',
|
||||
'opendrive',
|
||||
'swift',
|
||||
'pikpak',
|
||||
'pcloud',
|
||||
'pixeldrain',
|
||||
'premiumizeme',
|
||||
'putio',
|
||||
'protondrive',
|
||||
'quatrix',
|
||||
'seafile',
|
||||
'sugarsync',
|
||||
'storj',
|
||||
'webdav',
|
||||
'yandex',
|
||||
'zoho',
|
||||
] as const
|
||||
|
||||
export const SUPPORTS_ABOUT = [
|
||||
'box',
|
||||
'dropbox',
|
||||
'gofile',
|
||||
'drive',
|
||||
'filen',
|
||||
'hdfs',
|
||||
'internetarchive',
|
||||
'jottacloud',
|
||||
'koofr',
|
||||
'mailru',
|
||||
'mega',
|
||||
'azurefiles',
|
||||
'onedrive',
|
||||
'opendrive',
|
||||
'swift',
|
||||
'pcloud',
|
||||
'pikpak',
|
||||
'pixeldrain',
|
||||
'premiumizeme',
|
||||
'putio',
|
||||
'protondrive',
|
||||
'quatrix',
|
||||
'seafile',
|
||||
'sftp',
|
||||
'webdav',
|
||||
'yandex',
|
||||
'zoho',
|
||||
'local',
|
||||
] as const
|
||||
|
||||
export const SUPPORTS_LINK = [
|
||||
'b2',
|
||||
'box',
|
||||
'drive',
|
||||
'dropbox',
|
||||
'fichier',
|
||||
'filescom',
|
||||
'gofile',
|
||||
'imagekit',
|
||||
'internetarchive',
|
||||
'jottacloud',
|
||||
'koofr',
|
||||
'mailru',
|
||||
'mega',
|
||||
'onedrive',
|
||||
'pikpak',
|
||||
'pixeldrain',
|
||||
'premiumizeme',
|
||||
'pcloud',
|
||||
's3',
|
||||
'seafile',
|
||||
'storj',
|
||||
'sugarsync',
|
||||
'yandex',
|
||||
] as const
|
||||
|
||||
export function supportsPublicLink(backendType?: string | null): boolean {
|
||||
if (!backendType) return false
|
||||
return SUPPORTS_LINK.includes(backendType.toLowerCase() as any)
|
||||
}
|
||||
|
||||
export const CANNOT_PERSIST_EMPTY_FOLDERS = [
|
||||
's3',
|
||||
'gcs',
|
||||
'azureblob',
|
||||
'b2',
|
||||
'swift',
|
||||
'oracleobjectstorage',
|
||||
'oos',
|
||||
'qingstor',
|
||||
'storj',
|
||||
'memory',
|
||||
]
|
||||
|
||||
export function supportsPersistentEmptyFolders(backendType?: string | null) {
|
||||
if (!backendType) return true
|
||||
return !CANNOT_PERSIST_EMPTY_FOLDERS.includes(backendType.toLowerCase())
|
||||
}
|
||||
// Backend capabilities are no longer hardcoded here. They are read per-remote from the rclone RC
|
||||
// `operations/fsinfo` endpoint (see `fsInfoQueryOptions` / `hasFeature` in lib/hooks.ts), which is
|
||||
// authoritative and correct for wrapping backends (crypt/alias/union) that static type lists could
|
||||
// not express.
|
||||
|
||||
@@ -111,6 +111,9 @@ export default function RemoteEditDrawer({
|
||||
...updatedRemoteConfig,
|
||||
})
|
||||
)
|
||||
// Capabilities can change with the config (e.g. s3 provider, webdav vendor, a wrapped
|
||||
// backend's target), so drop the cached fsinfo probe and let consumers re-fetch.
|
||||
queryClient.invalidateQueries({ queryKey: ['remote', remoteName, 'fsinfo'] })
|
||||
onClose()
|
||||
},
|
||||
onError: onErrorDialog('Could not update remote', 'Unknown error occurred', {
|
||||
|
||||
@@ -341,8 +341,8 @@ export default function ScheduleEditDrawer({
|
||||
</Tabs>
|
||||
<span className="text-tiny text-default-400">
|
||||
{runMode === 'user'
|
||||
? 'Runs only while you are logged in, inside your session — OS keychain passwords and session-mounted drives work; fires while logged out are skipped. On macOS it runs as Rclone UI, so protected folders work once you grant the app access.'
|
||||
: 'Runs even while logged out, but outside your login session — no OS keychain or session-mounted drives, and protected folders on macOS need Full Disk Access for cron.'}
|
||||
? 'Runs only while you are logged in, inside your session. OS keychain passwords and session-mounted drives work; fires while logged out are skipped. On macOS it runs as Rclone UI, so protected folders work once you grant the app access.'
|
||||
: 'Runs even while logged out, but outside your login session. No OS keychain or session-mounted drives, and protected folders on macOS need Full Disk Access for cron.'}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { remoteConfigQueryOptions } from '../../../lib/hooks'
|
||||
import { supportsPublicLink } from '../../../lib/rclone/constants'
|
||||
import { fsInfoQueryOptions, hasFeature } from '../../../lib/hooks'
|
||||
import { useHostStore } from '../../../store/host.ts'
|
||||
import FileList from './FileList'
|
||||
import PanelToolbar, { type ToolbarButtons } from './PanelToolbar'
|
||||
@@ -89,12 +88,13 @@ const FilePanel = forwardRef<
|
||||
isActive,
|
||||
})
|
||||
|
||||
const remoteConfigQuery = useQuery({
|
||||
...remoteConfigQueryOptions(nav.selectedRemote),
|
||||
const fsInfoQuery = useQuery({
|
||||
...fsInfoQueryOptions(nav.selectedRemote),
|
||||
enabled: nav.isRemote,
|
||||
})
|
||||
|
||||
const canShare = supportsPublicLink(remoteConfigQuery.data?.type)
|
||||
// false while loading — matches the previous default (hide the share affordance until confirmed).
|
||||
const canShare = hasFeature(fsInfoQuery.data, 'PublicLink')
|
||||
|
||||
const { canCreateFolder, createFolder } = useCreateFolder(
|
||||
nav.selectedRemote,
|
||||
|
||||
@@ -3,26 +3,21 @@ import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { reportError } from '../../../lib/errors'
|
||||
import { getFsInfo } from '../../../lib/format'
|
||||
import { useRemoteConfig } from '../../../lib/hooks'
|
||||
import { hasFeature, useFsInfo } from '../../../lib/hooks'
|
||||
import rclone from '../../../lib/rclone/client'
|
||||
import { supportsPersistentEmptyFolders } from '../../../lib/rclone/constants'
|
||||
import type { RemoteString } from './types'
|
||||
import { RE_TRAILING_SEPARATORS } from './utils'
|
||||
|
||||
export default function useCreateFolder(remote: RemoteString, cwd: string, refresh: () => void) {
|
||||
const remoteConfigQuery = useRemoteConfig(remote)
|
||||
|
||||
const backendType = useMemo(() => {
|
||||
if (!remote || remote === 'UI_FAVORITES') return null
|
||||
if (remote === 'UI_LOCAL_FS') return 'local'
|
||||
return remoteConfigQuery.data?.type ?? null
|
||||
}, [remote, remoteConfigQuery.data])
|
||||
const fsInfoQuery = useFsInfo(remote)
|
||||
|
||||
const canCreateFolder = useMemo(() => {
|
||||
if (!remote || remote === 'UI_FAVORITES') return false
|
||||
if (remote === 'UI_LOCAL_FS') return true
|
||||
return supportsPersistentEmptyFolders(backendType)
|
||||
}, [remote, backendType])
|
||||
// Optimistic while probing: the previous static check defaulted to "supported" for unknown
|
||||
// types, so keep that until fsinfo actually says the backend can't persist empty folders.
|
||||
return fsInfoQuery.data ? hasFeature(fsInfoQuery.data, 'CanHaveEmptyDirectories') : true
|
||||
}, [remote, fsInfoQuery.data])
|
||||
|
||||
const createFolder = useCallback(async () => {
|
||||
if (!remote || remote === 'UI_FAVORITES') return
|
||||
|
||||
@@ -6,10 +6,10 @@ import { useSearchParams } from 'react-router-dom'
|
||||
import { onErrorDialog } from '../../lib/errors'
|
||||
import { getOptionsSubtitle } from '../../lib/flags'
|
||||
import { getRemoteName } from '../../lib/format'
|
||||
import { useFlags, useRemoteConfig } from '../../lib/hooks'
|
||||
import { hasFeature, useFlags, useFsInfo } from '../../lib/hooks'
|
||||
import { notify } from '../../lib/notifications'
|
||||
import { startDelete, startDryRun } from '../../lib/rclone/api'
|
||||
import { RCLONE_CONFIG_DEFAULTS, SUPPORTS_PURGE } from '../../lib/rclone/constants'
|
||||
import { RCLONE_CONFIG_DEFAULTS } from '../../lib/rclone/constants'
|
||||
import OperationWindowContent from '../components/OperationWindowContent'
|
||||
import OperationWindowFooter from '../components/OperationWindowFooter'
|
||||
import OptionsSection from '../components/OptionsSection'
|
||||
@@ -79,14 +79,12 @@ export default function Delete() {
|
||||
|
||||
const sourceRemoteName = useMemo(() => getRemoteName(sourceFs), [sourceFs])
|
||||
|
||||
const sourceRemoteConfigQuery = useRemoteConfig(sourceRemoteName)
|
||||
const sourceFsInfoQuery = useFsInfo(sourceRemoteName)
|
||||
|
||||
// false while loading — matches the previous default (offer the plain delete until purge is confirmed).
|
||||
const supportsPurge = useMemo(
|
||||
() =>
|
||||
sourceRemoteConfigQuery.data
|
||||
? SUPPORTS_PURGE.includes(sourceRemoteConfigQuery.data.type)
|
||||
: false,
|
||||
[sourceRemoteConfigQuery.data]
|
||||
() => hasFeature(sourceFsInfoQuery.data, 'Purge'),
|
||||
[sourceFsInfoQuery.data]
|
||||
)
|
||||
|
||||
const buildArgs = () => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Input,
|
||||
Spinner,
|
||||
} from '@heroui/react'
|
||||
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import {
|
||||
@@ -25,9 +25,8 @@ import { type ReactNode, startTransition, useEffect, useMemo, useState } from 'r
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { onErrorDialog } from '../../../lib/errors'
|
||||
import { formatBytes } from '../../../lib/format'
|
||||
import { remoteConfigQueryOptions } from '../../../lib/hooks'
|
||||
import { hasFeature, remoteConfigQueryOptions, useFsInfo } from '../../../lib/hooks'
|
||||
import rclone from '../../../lib/rclone/client'
|
||||
import { SUPPORTS_ABOUT } from '../../../lib/rclone/constants'
|
||||
import { usePersistedStore } from '../../../store/persisted'
|
||||
import RemoteAutoMountDrawer from '../../components/RemoteAutoMountDrawer'
|
||||
import RemoteCreateDrawer from '../../components/RemoteCreateDrawer'
|
||||
@@ -53,33 +52,10 @@ export default function RemotesSection() {
|
||||
|
||||
const remotes = useMemo(() => remotesQuery.data ?? [], [remotesQuery.data])
|
||||
|
||||
const remoteConfigQueries = useQueries({
|
||||
queries: remotes.map((remote) => ({
|
||||
...remoteConfigQueryOptions(remote),
|
||||
staleTime: 1000 * 60,
|
||||
})),
|
||||
})
|
||||
|
||||
const sortedRemotes = useMemo(() => {
|
||||
// useQueries preserves input order, so remoteConfigQueries[i] corresponds to remotes[i].
|
||||
const typeByRemote = new Map<string, string | null>()
|
||||
remotes.forEach((remote, i) => {
|
||||
typeByRemote.set(remote, remoteConfigQueries[i]?.data?.type ?? null)
|
||||
})
|
||||
|
||||
return [...remotes].sort((a, b) => {
|
||||
const aType = typeByRemote.get(a)
|
||||
const bType = typeByRemote.get(b)
|
||||
|
||||
const aSupportsAbout = aType ? SUPPORTS_ABOUT.includes(aType) : false
|
||||
const bSupportsAbout = bType ? SUPPORTS_ABOUT.includes(bType) : false
|
||||
|
||||
if (aSupportsAbout && !bSupportsAbout) return -1
|
||||
if (!aSupportsAbout && bSupportsAbout) return 1
|
||||
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
}, [remotes, remoteConfigQueries])
|
||||
const sortedRemotes = useMemo(
|
||||
() => [...remotes].sort((a, b) => a.localeCompare(b)),
|
||||
[remotes]
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
@@ -333,7 +309,9 @@ function RemoteCard({
|
||||
|
||||
const type = useMemo(() => remoteConfigData?.type ?? null, [remoteConfigData?.type])
|
||||
const provider = useMemo(() => remoteConfigData?.provider ?? null, [remoteConfigData?.provider])
|
||||
const supportsAbout = useMemo(() => !!type && SUPPORTS_ABOUT.includes(type), [type])
|
||||
|
||||
const fsInfoQuery = useFsInfo(remote)
|
||||
const supportsAbout = hasFeature(fsInfoQuery.data, 'About')
|
||||
|
||||
const { data: remoteAboutData } = useQuery({
|
||||
queryKey: ['remotes', remote, 'about'],
|
||||
|
||||
+31
-3
@@ -1,14 +1,16 @@
|
||||
import { Divider, Kbd, ScrollShadow, cn } from '@heroui/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQueries, useQuery } from '@tanstack/react-query'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { KeyboardEvent, MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { fsInfoQueryOptions } from '../../lib/hooks'
|
||||
import { fetchMountList, fetchServeList } from '../../lib/rclone/api'
|
||||
import rclone from '../../lib/rclone/client'
|
||||
import { openWindow } from '../../lib/window'
|
||||
import { type ResolvedToolbarResult, runToolbarEngine } from '../../toolbar/engine'
|
||||
import type { RcloneFeatures } from '../../types/rclone'
|
||||
|
||||
const toolbarWindow = getCurrentWebviewWindow()
|
||||
const isWindows = platform() === 'windows'
|
||||
@@ -111,6 +113,19 @@ export default function Toolbar() {
|
||||
const remotes = useMemo(() => remotesQuery.data ?? [], [remotesQuery.data])
|
||||
const remoteTypes = useMemo(() => remoteTypesQuery.data ?? {}, [remoteTypesQuery.data])
|
||||
|
||||
// Eager per-remote capability probes (operations/fsinfo), cached hard. Feeds the synchronous
|
||||
// engine so cleanup/purge can gate on the authoritative feature set. Unresolved/unreachable
|
||||
// remotes are simply absent from the map → those actions fall to their generic item.
|
||||
const fsInfoQueries = useQueries({ queries: remotes.map((remote) => fsInfoQueryOptions(remote)) })
|
||||
const capabilitiesByRemote = useMemo(() => {
|
||||
const map: Record<string, RcloneFeatures> = {}
|
||||
remotes.forEach((remote, i) => {
|
||||
const features = fsInfoQueries[i]?.data?.Features
|
||||
if (features) map[remote] = features
|
||||
})
|
||||
return map
|
||||
}, [remotes, fsInfoQueries])
|
||||
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [searchStringDebounced] = useDebounce(searchString, 40)
|
||||
|
||||
@@ -127,11 +142,24 @@ export default function Toolbar() {
|
||||
console.log(`${serveList?.length} serves`)
|
||||
console.log(`${vfsList?.length} vfses`)
|
||||
|
||||
const { results } = runToolbarEngine(searchStringDebounced, remotes, remoteTypes)
|
||||
const { results } = runToolbarEngine(
|
||||
searchStringDebounced,
|
||||
remotes,
|
||||
remoteTypes,
|
||||
capabilitiesByRemote
|
||||
)
|
||||
startTransition(() => {
|
||||
setEngineResults(results)
|
||||
})
|
||||
}, [mountList, serveList, vfsList, searchStringDebounced, remotes, remoteTypes])
|
||||
}, [
|
||||
mountList,
|
||||
serveList,
|
||||
vfsList,
|
||||
searchStringDebounced,
|
||||
remotes,
|
||||
remoteTypes,
|
||||
capabilitiesByRemote,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | undefined
|
||||
|
||||
+3
-7
@@ -8,7 +8,7 @@ import { notify } from '../lib/notifications'
|
||||
import queryClient from '../lib/query'
|
||||
import type { fetchMountList, fetchServeList } from '../lib/rclone/api'
|
||||
import rclone from '../lib/rclone/client'
|
||||
import { SERVE_TYPES, SUPPORTS_CLEANUP, SUPPORTS_PURGE } from '../lib/rclone/constants'
|
||||
import { SERVE_TYPES } from '../lib/rclone/constants'
|
||||
import { openFullWindow } from '../lib/window'
|
||||
import { selectCurrentHost, usePersistedStore } from '../store/persisted'
|
||||
import { COMMAND_CONFIG, COMMAND_DESCRIPTIONS, COMMAND_KEYWORDS } from './constants'
|
||||
@@ -656,9 +656,7 @@ const actions: ToolbarActionDefinition[] = [
|
||||
return []
|
||||
}
|
||||
|
||||
const supportedRemotePaths = paths.filter(
|
||||
(path) => path.remoteType && SUPPORTS_CLEANUP.includes(path.remoteType)
|
||||
)
|
||||
const supportedRemotePaths = paths.filter((path) => !!path.features?.CleanUp)
|
||||
|
||||
if (supportedRemotePaths.length === 0) {
|
||||
return [createBaseResult('Cleanup', COMMAND_DESCRIPTIONS.cleanup, {}, 36)]
|
||||
@@ -878,9 +876,7 @@ const actions: ToolbarActionDefinition[] = [
|
||||
return []
|
||||
}
|
||||
|
||||
const supportedPaths = paths.filter(
|
||||
(path) => path.remoteType && SUPPORTS_PURGE.includes(path.remoteType)
|
||||
)
|
||||
const supportedPaths = paths.filter((path) => !!path.features?.Purge)
|
||||
|
||||
if (supportedPaths.length === 0) {
|
||||
return [createBaseResult('Purge', COMMAND_DESCRIPTIONS.purge, {}, 36)]
|
||||
|
||||
+7
-3
@@ -1,4 +1,5 @@
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import type { RcloneFeatures } from '../types/rclone'
|
||||
import { getToolbarAction, getToolbarActions } from './actions'
|
||||
import type {
|
||||
ToolbarActionArgs,
|
||||
@@ -22,13 +23,14 @@ export interface ResolvedToolbarResult {
|
||||
export function runToolbarEngine(
|
||||
query: string,
|
||||
remotes: string[],
|
||||
remoteTypes?: Record<string, string>
|
||||
remoteTypes?: Record<string, string>,
|
||||
capabilitiesByRemote?: Record<string, RcloneFeatures>
|
||||
) {
|
||||
const actions = getToolbarActions()
|
||||
|
||||
const trimmed = query.trim()
|
||||
|
||||
const parsedPaths = extractPaths(trimmed, remotes, remoteTypes)
|
||||
const parsedPaths = extractPaths(trimmed, remotes, remoteTypes, capabilitiesByRemote)
|
||||
|
||||
const cleanedQuery = trimmed.replace(parsedPaths.map((path) => path.full).join(' '), '').trim()
|
||||
|
||||
@@ -120,7 +122,8 @@ const NON_WHITESPACE_TOKEN_REGEX = /^(\S+)/
|
||||
function extractPaths(
|
||||
input: string,
|
||||
remotes: string[],
|
||||
remoteTypes?: Record<string, string>
|
||||
remoteTypes?: Record<string, string>,
|
||||
capabilitiesByRemote?: Record<string, RcloneFeatures>
|
||||
): ToolbarActionPath[] {
|
||||
const seen = new Set<string>()
|
||||
const results: ToolbarActionPath[] = []
|
||||
@@ -178,6 +181,7 @@ function extractPaths(
|
||||
isLocal,
|
||||
remoteName,
|
||||
remoteType,
|
||||
features: remoteName ? capabilitiesByRemote?.[remoteName] : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { RcloneFeatures } from '../types/rclone'
|
||||
|
||||
export type ToolbarCommandId =
|
||||
| 'copy'
|
||||
| 'move'
|
||||
@@ -43,6 +45,9 @@ export interface ToolbarActionPath {
|
||||
isLocal: boolean
|
||||
remoteName?: string
|
||||
remoteType?: string
|
||||
// Authoritative backend capability set from `operations/fsinfo`; undefined until the probe
|
||||
// resolves (or for local paths), in which case capability-gated actions fall to their generic item.
|
||||
features?: RcloneFeatures
|
||||
}
|
||||
|
||||
export interface ToolbarActionContext {
|
||||
|
||||
Vendored
+21
@@ -26,3 +26,24 @@ export interface Backend {
|
||||
}
|
||||
|
||||
export type FlagValue = string | number | boolean | string[] | null
|
||||
|
||||
// Response shape of the rclone RC `operations/fsinfo` endpoint. The SDK types `Features` only as
|
||||
// an open `{ [k: string]: boolean }` map; this names the keys we actually gate on. Keys are the
|
||||
// PascalCase names rclone emits from `Features.Enabled()` (fs/features.go).
|
||||
export interface RcloneFeatures {
|
||||
About?: boolean
|
||||
Purge?: boolean
|
||||
CleanUp?: boolean
|
||||
PublicLink?: boolean
|
||||
CanHaveEmptyDirectories?: boolean
|
||||
[key: string]: boolean | undefined
|
||||
}
|
||||
|
||||
export interface RcloneFsInfo {
|
||||
Name?: string
|
||||
Root?: string
|
||||
String?: string
|
||||
Precision?: number
|
||||
Hashes?: string[]
|
||||
Features?: RcloneFeatures
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user