mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
fix(app-assets): restore atomic batch upload for nested folder targets
The previous nested folder upload flow bypassed the backend batch-upload contract when parentId was set. Instead of creating the whole metadata tree in one backend operation, the frontend recursively called createFolder/getFileUploadUrl for each node. That introduced two regressions for uploads into subfolders: - consistency regression: mid-sequence failures could leave partially created folder trees under the destination folder - performance regression: metadata creation degraded from a single batch request to O(files + folders) round-trips before file bytes were uploaded This change moves nested uploads back to the original batch semantics: - add optional parent_id support to app asset batch-upload payload - create the whole nested tree under the target parent in AppAssetService.batch_create_from_tree - pass parentId through useBatchUpload instead of using per-node createFolder/getFileUploadUrl calls - remove the now-unnecessary useBatchUploadOperation wrapper - add a backend unit test covering batch tree creation under an existing parent folder After this change, both root uploads and subfolder uploads use the same single-request metadata creation path, preserving atomic tree creation semantics and avoiding avoidable metadata round-trips.
This commit is contained in:
@ -1,147 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { BatchUploadNodeInput, BatchUploadNodeOutput } from '@/types/app-asset'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { uploadToPresignedUrl } from '@/service/upload-to-presigned-url'
|
||||
import { useBatchUpload } from '@/service/use-app-asset'
|
||||
|
||||
type BatchUploadOperationVariables = {
|
||||
appId: string
|
||||
tree: BatchUploadNodeInput[]
|
||||
files: Map<string, File>
|
||||
parentId?: string | null
|
||||
onProgress?: (uploaded: number, total: number) => void
|
||||
}
|
||||
|
||||
type BatchUploadTask = {
|
||||
file: File
|
||||
url: string
|
||||
}
|
||||
|
||||
const uploadBatchUploadTasks = async ({
|
||||
tasks,
|
||||
onProgress,
|
||||
}: {
|
||||
tasks: BatchUploadTask[]
|
||||
onProgress?: (uploaded: number, total: number) => void
|
||||
}) => {
|
||||
let completed = 0
|
||||
const total = tasks.length
|
||||
|
||||
await Promise.all(
|
||||
tasks.map(async (task) => {
|
||||
await uploadToPresignedUrl({
|
||||
file: task.file,
|
||||
uploadUrl: task.url,
|
||||
})
|
||||
completed++
|
||||
onProgress?.(completed, total)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const createBatchUploadTreeInParent = async ({
|
||||
appId,
|
||||
tree,
|
||||
files,
|
||||
parentId,
|
||||
pathPrefix = '',
|
||||
}: {
|
||||
appId: string
|
||||
tree: BatchUploadNodeInput[]
|
||||
files: Map<string, File>
|
||||
parentId: string
|
||||
pathPrefix?: string
|
||||
}): Promise<{ nodes: BatchUploadNodeOutput[], tasks: BatchUploadTask[] }> => {
|
||||
const nodes: BatchUploadNodeOutput[] = []
|
||||
const tasks: BatchUploadTask[] = []
|
||||
|
||||
for (const inputNode of tree) {
|
||||
const sourcePath = pathPrefix ? `${pathPrefix}/${inputNode.name}` : inputNode.name
|
||||
|
||||
if (inputNode.node_type === 'folder') {
|
||||
const folder = await consoleClient.appAsset.createFolder({
|
||||
params: { appId },
|
||||
body: { name: inputNode.name, parent_id: parentId },
|
||||
})
|
||||
|
||||
const childrenResult = await createBatchUploadTreeInParent({
|
||||
appId,
|
||||
tree: inputNode.children ?? [],
|
||||
files,
|
||||
parentId: folder.id,
|
||||
pathPrefix: sourcePath,
|
||||
})
|
||||
|
||||
nodes.push({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
node_type: folder.node_type,
|
||||
size: folder.size,
|
||||
children: childrenResult.nodes,
|
||||
})
|
||||
tasks.push(...childrenResult.tasks)
|
||||
continue
|
||||
}
|
||||
|
||||
const file = files.get(sourcePath)
|
||||
if (!file)
|
||||
throw new Error(`Missing file for batch upload path: ${sourcePath}`)
|
||||
|
||||
const { node, upload_url } = await consoleClient.appAsset.getFileUploadUrl({
|
||||
params: { appId },
|
||||
body: {
|
||||
name: inputNode.name,
|
||||
size: inputNode.size ?? file.size,
|
||||
parent_id: parentId,
|
||||
},
|
||||
})
|
||||
|
||||
nodes.push({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
node_type: node.node_type,
|
||||
size: node.size,
|
||||
children: [],
|
||||
upload_url,
|
||||
})
|
||||
tasks.push({ file, url: upload_url })
|
||||
}
|
||||
|
||||
return { nodes, tasks }
|
||||
}
|
||||
|
||||
export function useBatchUploadOperation() {
|
||||
const queryClient = useQueryClient()
|
||||
const batchUpload = useBatchUpload()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: consoleQuery.appAsset.batchUpload.mutationKey(),
|
||||
mutationFn: async (variables: BatchUploadOperationVariables): Promise<BatchUploadNodeOutput[]> => {
|
||||
if (!variables.parentId)
|
||||
return batchUpload.mutateAsync(variables)
|
||||
|
||||
try {
|
||||
const result = await createBatchUploadTreeInParent({
|
||||
appId: variables.appId,
|
||||
tree: variables.tree,
|
||||
files: variables.files,
|
||||
parentId: variables.parentId,
|
||||
})
|
||||
|
||||
await uploadBatchUploadTasks({
|
||||
tasks: result.tasks,
|
||||
onProgress: variables.onProgress,
|
||||
})
|
||||
|
||||
return result.nodes
|
||||
}
|
||||
finally {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.appAsset.tree.key({ type: 'query', input: { params: { appId: variables.appId } } }),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -5,12 +5,12 @@ import type { SkillEditorSliceShape } from '@/app/components/workflow/store/work
|
||||
import type { BatchUploadNodeInput } from '@/types/app-asset'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import {
|
||||
useBatchUpload,
|
||||
useCreateAppAssetFolder,
|
||||
useUploadFileWithPresignedUrl,
|
||||
} from '@/service/use-app-asset'
|
||||
import { prepareSkillUploadFile } from '../../../utils/skill-upload-utils'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
import { useBatchUploadOperation } from './use-batch-upload-operation'
|
||||
|
||||
type UseCreateOperationsOptions = {
|
||||
parentId: string | null
|
||||
@ -34,7 +34,7 @@ export function useCreateOperations({
|
||||
|
||||
const { isPending: isCreateFolderPending } = useCreateAppAssetFolder()
|
||||
const { mutateAsync: uploadFileAsync, isPending: isUploadFilePending } = useUploadFileWithPresignedUrl()
|
||||
const { mutateAsync: batchUploadAsync, isPending: isBatchUploadPending } = useBatchUploadOperation()
|
||||
const { mutateAsync: batchUploadAsync, isPending: isBatchUploadPending } = useBatchUpload()
|
||||
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
|
||||
const handleNewFile = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user