mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
refactor(app-asset): migrate file upload to presigned URL and batch upload
- Replace FormData file upload with presigned URL two-step upload - Add batch-upload contract for folder uploads (reduces N+M to 1+M requests) - Remove deprecated createFile contract and useCreateAppAssetFile hook - Remove checksum field from AppAssetNode and AppAssetTreeView types - Add upload-to-presigned-url utility for direct storage uploads
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
|
||||
// Handles file/folder creation and upload operations
|
||||
|
||||
import type { StoreApi } from 'zustand'
|
||||
import type { SkillEditorSliceShape } from '@/app/components/workflow/store/workflow/skill-editor/types'
|
||||
import type { BatchUploadNodeInput } from '@/types/app-asset'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
useCreateAppAssetFile,
|
||||
useBatchUpload,
|
||||
useCreateAppAssetFolder,
|
||||
useUploadFileWithPresignedUrl,
|
||||
} from '@/service/use-app-asset'
|
||||
|
||||
type UseCreateOperationsOptions = {
|
||||
@@ -34,7 +34,8 @@ export function useCreateOperations({
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const createFolder = useCreateAppAssetFolder()
|
||||
const createFile = useCreateAppAssetFile()
|
||||
const uploadFile = useUploadFileWithPresignedUrl()
|
||||
const batchUpload = useBatchUpload()
|
||||
|
||||
const handleNewFile = useCallback(() => {
|
||||
storeApi.getState().startCreateNode('file', parentId)
|
||||
@@ -56,9 +57,8 @@ export function useCreateOperations({
|
||||
try {
|
||||
await Promise.all(
|
||||
files.map(file =>
|
||||
createFile.mutateAsync({
|
||||
uploadFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId,
|
||||
}),
|
||||
@@ -80,7 +80,7 @@ export function useCreateOperations({
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, onClose, parentId, t])
|
||||
}, [appId, uploadFile, onClose, parentId, t])
|
||||
|
||||
const handleFolderChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
@@ -90,78 +90,52 @@ export function useCreateOperations({
|
||||
}
|
||||
|
||||
try {
|
||||
const folders = new Set<string>()
|
||||
const fileMap = new Map<string, File>()
|
||||
const tree: BatchUploadNodeInput[] = []
|
||||
const folderMap = new Map<string, BatchUploadNodeInput>()
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = getRelativePath(file)
|
||||
const parts = relativePath.split('/')
|
||||
fileMap.set(relativePath, file)
|
||||
|
||||
if (parts.length > 1) {
|
||||
let folderPath = ''
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
folderPath = folderPath ? `${folderPath}/${parts[i]}` : parts[i]
|
||||
folders.add(folderPath)
|
||||
const parts = relativePath.split('/')
|
||||
let currentLevel = tree
|
||||
let currentPath = ''
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const isLastPart = i === parts.length - 1
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||
|
||||
if (isLastPart) {
|
||||
currentLevel.push({
|
||||
name: part,
|
||||
node_type: 'file',
|
||||
size: file.size,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let folder = folderMap.get(currentPath)
|
||||
if (!folder) {
|
||||
folder = {
|
||||
name: part,
|
||||
node_type: 'folder',
|
||||
children: [],
|
||||
}
|
||||
folderMap.set(currentPath, folder)
|
||||
currentLevel.push(folder)
|
||||
}
|
||||
currentLevel = folder.children!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedFolders = Array.from(folders).sort((a, b) => {
|
||||
return a.split('/').length - b.split('/').length
|
||||
await batchUpload.mutateAsync({
|
||||
appId,
|
||||
tree,
|
||||
files: fileMap,
|
||||
})
|
||||
|
||||
const folderIdMap = new Map<string, string | null>()
|
||||
folderIdMap.set('', parentId)
|
||||
|
||||
const foldersByDepth = new Map<number, string[]>()
|
||||
for (const folderPath of sortedFolders) {
|
||||
const depth = folderPath.split('/').length
|
||||
const group = foldersByDepth.get(depth)
|
||||
if (group)
|
||||
group.push(folderPath)
|
||||
else
|
||||
foldersByDepth.set(depth, [folderPath])
|
||||
}
|
||||
|
||||
for (const [, foldersAtDepth] of foldersByDepth) {
|
||||
const createdFolders = await Promise.all(
|
||||
foldersAtDepth.map(async (folderPath) => {
|
||||
const parts = folderPath.split('/')
|
||||
const folderName = parts[parts.length - 1]
|
||||
const parentPath = parts.slice(0, -1).join('/')
|
||||
const parentFolderId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
const result = await createFolder.mutateAsync({
|
||||
appId,
|
||||
payload: {
|
||||
name: folderName,
|
||||
parent_id: parentFolderId,
|
||||
},
|
||||
})
|
||||
|
||||
return { folderPath, id: result.id }
|
||||
}),
|
||||
)
|
||||
|
||||
for (const { folderPath, id } of createdFolders)
|
||||
folderIdMap.set(folderPath, id)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
files.map((file) => {
|
||||
const relativePath = getRelativePath(file)
|
||||
const parts = relativePath.split('/')
|
||||
const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : ''
|
||||
const targetParentId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
return createFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId: targetParentId,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.folderUploaded'),
|
||||
@@ -177,12 +151,12 @@ export function useCreateOperations({
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, createFolder, onClose, parentId, t])
|
||||
}, [appId, batchUpload, onClose, t])
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
folderInputRef,
|
||||
isCreating: createFile.isPending || createFolder.isPending,
|
||||
isCreating: uploadFile.isPending || createFolder.isPending || batchUpload.isPending,
|
||||
handleNewFile,
|
||||
handleNewFolder,
|
||||
handleFileChange,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useCreateAppAssetFile } from '@/service/use-app-asset'
|
||||
import { useUploadFileWithPresignedUrl } from '@/service/use-app-asset'
|
||||
import { ROOT_ID } from '../constants'
|
||||
|
||||
type FileDropTarget = {
|
||||
@@ -21,7 +21,7 @@ export function useFileDrop() {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const storeApi = useWorkflowStore()
|
||||
const createFile = useCreateAppAssetFile()
|
||||
const uploadFile = useUploadFileWithPresignedUrl()
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent, target: FileDropTarget) => {
|
||||
e.preventDefault()
|
||||
@@ -80,9 +80,8 @@ export function useFileDrop() {
|
||||
try {
|
||||
await Promise.all(
|
||||
files.map(file =>
|
||||
createFile.mutateAsync({
|
||||
uploadFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId: targetFolderId,
|
||||
}),
|
||||
@@ -100,12 +99,12 @@ export function useFileDrop() {
|
||||
message: t('skillSidebar.menu.uploadError'),
|
||||
})
|
||||
}
|
||||
}, [appId, createFile, t, storeApi])
|
||||
}, [appId, uploadFile, t, storeApi])
|
||||
|
||||
return {
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
isUploading: createFile.isPending,
|
||||
isUploading: uploadFile.isPending,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useCreateAppAssetFile,
|
||||
useCreateAppAssetFolder,
|
||||
useRenameAppAssetNode,
|
||||
useUploadFileWithPresignedUrl,
|
||||
} from '@/service/use-app-asset'
|
||||
import { getFileExtension, isTextLikeFile } from '../utils/file-utils'
|
||||
import { createDraftTreeNode, insertDraftTreeNode } from '../utils/tree-utils'
|
||||
@@ -35,7 +35,7 @@ export function useInlineCreateNode({
|
||||
const pendingCreateNode = useStore(s => s.pendingCreateNode)
|
||||
const storeApi = useWorkflowStore()
|
||||
|
||||
const createFile = useCreateAppAssetFile()
|
||||
const uploadFile = useUploadFileWithPresignedUrl()
|
||||
const createFolder = useCreateAppAssetFolder()
|
||||
const renameNode = useRenameAppAssetNode()
|
||||
|
||||
@@ -79,9 +79,8 @@ export function useInlineCreateNode({
|
||||
else {
|
||||
const emptyBlob = new Blob([''], { type: 'text/plain' })
|
||||
const file = new File([emptyBlob], trimmedName)
|
||||
const createdFile = await createFile.mutateAsync({
|
||||
const createdFile = await uploadFile.mutateAsync({
|
||||
appId,
|
||||
name: trimmedName,
|
||||
file,
|
||||
parentId: pendingCreateParentId,
|
||||
})
|
||||
@@ -123,7 +122,7 @@ export function useInlineCreateNode({
|
||||
})
|
||||
}, [
|
||||
appId,
|
||||
createFile,
|
||||
uploadFile,
|
||||
createFolder,
|
||||
pendingCreateId,
|
||||
pendingCreateParentId,
|
||||
|
||||
@@ -161,7 +161,6 @@ export function createDraftTreeNode(options: DraftTreeNodeOptions): AppAssetTree
|
||||
path: '',
|
||||
extension: '',
|
||||
size: 0,
|
||||
checksum: '',
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import type {
|
||||
AppAssetNode,
|
||||
AppAssetPublishResponse,
|
||||
AppAssetTreeResponse,
|
||||
BatchUploadPayload,
|
||||
BatchUploadResponse,
|
||||
CreateFolderPayload,
|
||||
FileUploadUrlResponse,
|
||||
GetFileUploadUrlPayload,
|
||||
MoveNodePayload,
|
||||
RenameNodePayload,
|
||||
ReorderNodePayload,
|
||||
@@ -33,16 +37,6 @@ export const createFolderContract = base
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const createFileContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
}>())
|
||||
.output(type<AppAssetNode>())
|
||||
|
||||
export const getFileContentContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files/{nodeId}',
|
||||
@@ -126,3 +120,25 @@ export const publishContract = base
|
||||
params: { appId: string }
|
||||
}>())
|
||||
.output(type<AppAssetPublishResponse>())
|
||||
|
||||
export const getFileUploadUrlContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/files/upload',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
body: GetFileUploadUrlPayload
|
||||
}>())
|
||||
.output(type<FileUploadUrlResponse>())
|
||||
|
||||
export const batchUploadContract = base
|
||||
.route({
|
||||
path: '/apps/{appId}/assets/batch-upload',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: { appId: string }
|
||||
body: BatchUploadPayload
|
||||
}>())
|
||||
.output(type<BatchUploadResponse>())
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import {
|
||||
createFileContract,
|
||||
batchUploadContract,
|
||||
createFolderContract,
|
||||
deleteNodeContract,
|
||||
getFileContentContract,
|
||||
getFileDownloadUrlContract,
|
||||
getFileUploadUrlContract,
|
||||
moveNodeContract,
|
||||
publishContract,
|
||||
renameNodeContract,
|
||||
@@ -52,7 +53,6 @@ export const consoleRouterContract = {
|
||||
appAsset: {
|
||||
tree: treeContract,
|
||||
createFolder: createFolderContract,
|
||||
createFile: createFileContract,
|
||||
getFileContent: getFileContentContract,
|
||||
getFileDownloadUrl: getFileDownloadUrlContract,
|
||||
updateFileContent: updateFileContentContract,
|
||||
@@ -61,6 +61,8 @@ export const consoleRouterContract = {
|
||||
moveNode: moveNodeContract,
|
||||
reorderNode: reorderNodeContract,
|
||||
publish: publishContract,
|
||||
getFileUploadUrl: getFileUploadUrlContract,
|
||||
batchUpload: batchUploadContract,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
44
web/service/upload-to-presigned-url.ts
Normal file
44
web/service/upload-to-presigned-url.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
type UploadToPresignedUrlOptions = {
|
||||
file: File
|
||||
uploadUrl: string
|
||||
onProgress?: (progress: number) => void
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export async function uploadToPresignedUrl({
|
||||
file,
|
||||
uploadUrl,
|
||||
onProgress,
|
||||
signal,
|
||||
}: UploadToPresignedUrlOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => {
|
||||
xhr.abort()
|
||||
reject(new DOMException('Upload aborted', 'AbortError'))
|
||||
})
|
||||
}
|
||||
|
||||
xhr.open('PUT', uploadUrl)
|
||||
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream')
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable && onProgress)
|
||||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300)
|
||||
resolve()
|
||||
else
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`))
|
||||
}
|
||||
|
||||
xhr.onerror = () => reject(new Error('Upload network error'))
|
||||
xhr.ontimeout = () => reject(new Error('Upload timeout'))
|
||||
|
||||
xhr.send(file)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
AppAssetNode,
|
||||
AppAssetTreeResponse,
|
||||
BatchUploadNodeInput,
|
||||
BatchUploadNodeOutput,
|
||||
CreateFolderPayload,
|
||||
GetFileUploadUrlPayload,
|
||||
MoveNodePayload,
|
||||
RenameNodePayload,
|
||||
ReorderNodePayload,
|
||||
@@ -14,6 +17,7 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { upload } from './base'
|
||||
import { uploadToPresignedUrl } from './upload-to-presigned-url'
|
||||
|
||||
type UseGetAppAssetTreeOptions<TData = AppAssetTreeResponse> = {
|
||||
select?: (data: AppAssetTreeResponse) => TData
|
||||
@@ -49,53 +53,6 @@ export const useCreateAppAssetFolder = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateAppAssetFile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.createFile.mutationKey(),
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
name,
|
||||
file,
|
||||
parentId,
|
||||
onProgress,
|
||||
}: {
|
||||
appId: string
|
||||
name: string
|
||||
file: File
|
||||
parentId?: string | null
|
||||
onProgress?: (progress: number) => void
|
||||
}): Promise<AppAssetNode> => {
|
||||
const formData = new FormData()
|
||||
formData.append('name', name)
|
||||
formData.append('file', file)
|
||||
if (parentId)
|
||||
formData.append('parent_id', parentId)
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
return upload(
|
||||
{
|
||||
xhr,
|
||||
data: formData,
|
||||
onprogress: onProgress
|
||||
? (e) => {
|
||||
if (e.lengthComputable)
|
||||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
false,
|
||||
`/apps/${appId}/assets/files`,
|
||||
) as Promise<AppAssetNode>
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetAppAssetFileContent = (appId: string, nodeId: string, options?: { enabled?: boolean }) => {
|
||||
return useQuery({
|
||||
queryKey: consoleQuery.appAsset.getFileContent.queryKey({ input: { params: { appId, nodeId } } }),
|
||||
@@ -310,3 +267,104 @@ export const usePublishAppAssets = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUploadFileWithPresignedUrl = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.getFileUploadUrl.mutationKey(),
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
file,
|
||||
parentId,
|
||||
onProgress,
|
||||
}: {
|
||||
appId: string
|
||||
file: File
|
||||
parentId?: string | null
|
||||
onProgress?: (progress: number) => void
|
||||
}): Promise<AppAssetNode> => {
|
||||
const payload: GetFileUploadUrlPayload = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
parent_id: parentId,
|
||||
}
|
||||
|
||||
const { node, upload_url } = await consoleClient.appAsset.getFileUploadUrl({
|
||||
params: { appId },
|
||||
body: payload,
|
||||
})
|
||||
|
||||
await uploadToPresignedUrl({
|
||||
file,
|
||||
uploadUrl: upload_url,
|
||||
onProgress,
|
||||
})
|
||||
|
||||
return node
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useBatchUpload = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.batchUpload.mutationKey(),
|
||||
mutationFn: async ({
|
||||
appId,
|
||||
tree,
|
||||
files,
|
||||
onProgress,
|
||||
}: {
|
||||
appId: string
|
||||
tree: BatchUploadNodeInput[]
|
||||
files: Map<string, File>
|
||||
onProgress?: (uploaded: number, total: number) => void
|
||||
}): Promise<void> => {
|
||||
const response = await consoleClient.appAsset.batchUpload({
|
||||
params: { appId },
|
||||
body: { children: tree },
|
||||
})
|
||||
|
||||
const uploadTasks: Array<{ path: string, file: File, url: string }> = []
|
||||
|
||||
const extractUploads = (nodes: BatchUploadNodeOutput[], pathPrefix: string = '') => {
|
||||
for (const node of nodes) {
|
||||
const currentPath = pathPrefix ? `${pathPrefix}/${node.name}` : node.name
|
||||
if (node.upload_url) {
|
||||
const file = files.get(currentPath)
|
||||
if (file)
|
||||
uploadTasks.push({ path: currentPath, file, url: node.upload_url })
|
||||
}
|
||||
if (node.children && node.children.length > 0)
|
||||
extractUploads(node.children, currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
extractUploads(response.children)
|
||||
|
||||
let completed = 0
|
||||
const total = uploadTasks.length
|
||||
|
||||
await Promise.all(
|
||||
uploadTasks.map(async (task) => {
|
||||
await uploadToPresignedUrl({
|
||||
file: task.file,
|
||||
uploadUrl: task.url,
|
||||
})
|
||||
completed++
|
||||
onProgress?.(completed, total)
|
||||
}),
|
||||
)
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ export type AppAssetNode = {
|
||||
extension: string
|
||||
/** File size in bytes, 0 for folders */
|
||||
size: number
|
||||
/** SHA-256 checksum of file content, empty for folders */
|
||||
checksum: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,8 +48,6 @@ export type AppAssetTreeView = {
|
||||
extension: string
|
||||
/** File size in bytes */
|
||||
size: number
|
||||
/** SHA-256 checksum */
|
||||
checksum: string
|
||||
/** Child nodes (for folders) */
|
||||
children: AppAssetTreeView[]
|
||||
}
|
||||
@@ -138,3 +134,56 @@ export type ReorderNodePayload = {
|
||||
/** Place after this node ID, null for first position */
|
||||
after_node_id: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for getting file upload URL
|
||||
*/
|
||||
export type GetFileUploadUrlPayload = {
|
||||
name: string
|
||||
size: number
|
||||
parent_id?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for file upload URL request
|
||||
*/
|
||||
export type FileUploadUrlResponse = {
|
||||
node: AppAssetNode
|
||||
upload_url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Input node structure for batch upload
|
||||
*/
|
||||
export type BatchUploadNodeInput = {
|
||||
name: string
|
||||
node_type: AssetNodeType
|
||||
size?: number
|
||||
children?: BatchUploadNodeInput[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Output node structure from batch upload (with IDs and upload URLs)
|
||||
*/
|
||||
export type BatchUploadNodeOutput = {
|
||||
id: string
|
||||
name: string
|
||||
node_type: AssetNodeType
|
||||
size: number
|
||||
children: BatchUploadNodeOutput[]
|
||||
upload_url?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payload for batch upload
|
||||
*/
|
||||
export type BatchUploadPayload = {
|
||||
children: BatchUploadNodeInput[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for batch upload request
|
||||
*/
|
||||
export type BatchUploadResponse = {
|
||||
children: BatchUploadNodeOutput[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user