mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08: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: [],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user