mirror of
https://github.com/langgenius/dify.git
synced 2026-02-22 19:15:47 +08:00
feat(sandbox): add sandbox file API service layer
- Add types for sandbox file API (SandboxFileNode, SandboxFileDownloadTicket) - Add oRPC contracts for listFiles and downloadFile endpoints - Add TanStack Query hooks (useGetSandboxFiles, useDownloadSandboxFile) - Add useSandboxFilesTree hook with flat-to-tree conversion
This commit is contained in:
30
web/contract/console/sandbox-file.ts
Normal file
30
web/contract/console/sandbox-file.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type {
|
||||
SandboxFileDownloadRequest,
|
||||
SandboxFileDownloadTicket,
|
||||
SandboxFileListQuery,
|
||||
SandboxFileNode,
|
||||
} from '@/types/sandbox-file'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export const listFilesContract = base
|
||||
.route({
|
||||
path: '/sandboxes/{sandboxId}/files',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: { sandboxId: string }
|
||||
query?: SandboxFileListQuery
|
||||
}>())
|
||||
.output(type<SandboxFileNode[]>())
|
||||
|
||||
export const downloadFileContract = base
|
||||
.route({
|
||||
path: '/sandboxes/{sandboxId}/files/download',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { sandboxId: string }
|
||||
body: SandboxFileDownloadRequest
|
||||
}>())
|
||||
.output(type<SandboxFileDownloadTicket>())
|
||||
@ -16,6 +16,10 @@ import {
|
||||
} from './console/app-asset'
|
||||
import { workflowOnlineUsersContract } from './console/apps'
|
||||
import { bindPartnerStackContract, invoicesContract } from './console/billing'
|
||||
import {
|
||||
downloadFileContract,
|
||||
listFilesContract,
|
||||
} from './console/sandbox-file'
|
||||
import {
|
||||
activateSandboxProviderContract,
|
||||
deleteSandboxProviderConfigContract,
|
||||
@ -62,6 +66,10 @@ export const consoleRouterContract = {
|
||||
deleteSandboxProviderConfig: deleteSandboxProviderConfigContract,
|
||||
activateSandboxProvider: activateSandboxProviderContract,
|
||||
},
|
||||
sandboxFile: {
|
||||
listFiles: listFilesContract,
|
||||
downloadFile: downloadFileContract,
|
||||
},
|
||||
appAsset: {
|
||||
tree: treeContract,
|
||||
createFolder: createFolderContract,
|
||||
|
||||
121
web/service/use-sandbox-file.ts
Normal file
121
web/service/use-sandbox-file.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import type {
|
||||
SandboxFileListQuery,
|
||||
SandboxFileNode,
|
||||
SandboxFileTreeNode,
|
||||
} from '@/types/sandbox-file'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
|
||||
type UseGetSandboxFilesOptions = {
|
||||
path?: string
|
||||
recursive?: boolean
|
||||
enabled?: boolean
|
||||
refetchInterval?: number | false
|
||||
}
|
||||
|
||||
export function useGetSandboxFiles(
|
||||
sandboxId: string | undefined,
|
||||
options?: UseGetSandboxFilesOptions,
|
||||
) {
|
||||
const query: SandboxFileListQuery = {
|
||||
path: options?.path,
|
||||
recursive: options?.recursive,
|
||||
}
|
||||
|
||||
return useQuery({
|
||||
queryKey: consoleQuery.sandboxFile.listFiles.queryKey({
|
||||
input: { params: { sandboxId: sandboxId! }, query },
|
||||
}),
|
||||
queryFn: () => consoleClient.sandboxFile.listFiles({
|
||||
params: { sandboxId: sandboxId! },
|
||||
query,
|
||||
}),
|
||||
enabled: !!sandboxId && (options?.enabled ?? true),
|
||||
refetchInterval: options?.refetchInterval,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDownloadSandboxFile(sandboxId: string | undefined) {
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.sandboxFile.downloadFile.mutationKey(),
|
||||
mutationFn: (path: string) => {
|
||||
if (!sandboxId)
|
||||
throw new Error('sandboxId is required')
|
||||
return consoleClient.sandboxFile.downloadFile({
|
||||
params: { sandboxId },
|
||||
body: { path },
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[] {
|
||||
const nodeMap = new Map<string, SandboxFileTreeNode>()
|
||||
const roots: SandboxFileTreeNode[] = []
|
||||
|
||||
const sorted = [...nodes].sort((a, b) =>
|
||||
a.path.split('/').length - b.path.split('/').length,
|
||||
)
|
||||
|
||||
for (const node of sorted) {
|
||||
const parts = node.path.split('/')
|
||||
const name = parts[parts.length - 1]
|
||||
const parentPath = parts.slice(0, -1).join('/')
|
||||
|
||||
const treeNode: SandboxFileTreeNode = {
|
||||
id: node.path,
|
||||
name,
|
||||
path: node.path,
|
||||
node_type: node.is_dir ? 'folder' : 'file',
|
||||
size: node.size,
|
||||
mtime: node.mtime,
|
||||
children: [],
|
||||
}
|
||||
|
||||
nodeMap.set(node.path, treeNode)
|
||||
|
||||
if (parentPath === '') {
|
||||
roots.push(treeNode)
|
||||
}
|
||||
else {
|
||||
const parent = nodeMap.get(parentPath)
|
||||
if (parent)
|
||||
parent.children.push(treeNode)
|
||||
}
|
||||
}
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
export function useSandboxFilesTree(
|
||||
sandboxId: string | undefined,
|
||||
options?: UseGetSandboxFilesOptions,
|
||||
) {
|
||||
const { data, isLoading, error, refetch } = useGetSandboxFiles(sandboxId, {
|
||||
...options,
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
if (!data)
|
||||
return undefined
|
||||
return buildTreeFromFlatList(data)
|
||||
}, [data])
|
||||
|
||||
const hasFiles = useMemo(() => {
|
||||
return (data?.length ?? 0) > 0
|
||||
}, [data])
|
||||
|
||||
return {
|
||||
data: treeData,
|
||||
flatData: data,
|
||||
hasFiles,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
71
web/types/sandbox-file.ts
Normal file
71
web/types/sandbox-file.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Sandbox File Types
|
||||
*
|
||||
* Types for sandbox file API - file listing and download operations.
|
||||
* These files are generated by agent during test runs and may be cleared later.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sandbox file node from API (flat format)
|
||||
* Returned by GET /sandboxes/{sandbox_id}/files
|
||||
*/
|
||||
export type SandboxFileNode = {
|
||||
/** Relative path (POSIX format), e.g. "folder/file.txt" */
|
||||
path: string
|
||||
/** Whether this is a directory */
|
||||
is_dir: boolean
|
||||
/** File size in bytes (null for directories) */
|
||||
size: number | null
|
||||
/** Modification timestamp in seconds (null for some directories) */
|
||||
mtime: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Download ticket returned by POST /sandboxes/{sandbox_id}/files/download
|
||||
*/
|
||||
export type SandboxFileDownloadTicket = {
|
||||
/** Signed download URL */
|
||||
download_url: string
|
||||
/** Expiration time in seconds */
|
||||
expires_in: number
|
||||
/** Export ID (16-char hex) */
|
||||
export_id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tree node for frontend rendering (converted from flat list)
|
||||
*/
|
||||
export type SandboxFileTreeNode = {
|
||||
/** Unique ID (uses path as ID) */
|
||||
id: string
|
||||
/** File/folder name extracted from path */
|
||||
name: string
|
||||
/** Full relative path */
|
||||
path: string
|
||||
/** Node type for compatibility with existing tree components */
|
||||
node_type: 'file' | 'folder'
|
||||
/** File size in bytes (null for directories) */
|
||||
size: number | null
|
||||
/** Modification timestamp */
|
||||
mtime: number | null
|
||||
/** Child nodes (for folders) */
|
||||
children: SandboxFileTreeNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for listing files
|
||||
*/
|
||||
export type SandboxFileListQuery = {
|
||||
/** Workspace-relative path, defaults to "." */
|
||||
path?: string
|
||||
/** Whether to list recursively */
|
||||
recursive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for downloading a file
|
||||
*/
|
||||
export type SandboxFileDownloadRequest = {
|
||||
/** Workspace-relative file path */
|
||||
path: string
|
||||
}
|
||||
Reference in New Issue
Block a user