@@ -38,6 +38,9 @@ import { asOrchestrationError, OrchestrationError } from '@/lib/core/orchestrati
3838import { generateRequestId } from '@/lib/core/utils/request'
3939import { generateRestoreName } from '@/lib/core/utils/restore-name'
4040import type { DbOrTx } from '@/lib/db/types'
41+ import { acquireFolderMutationLock } from '@/lib/folders/locks'
42+ import { parseFolderPath } from '@/lib/folders/paths'
43+ import { loadActiveFolderPathIndex , resolveFolderPathFromIndex } from '@/lib/folders/queries'
4144import { mergeEditIntoLiveFileDoc , notifyWorkspaceFilesChanged } from '@/lib/realtime/notify'
4245import { getServePathPrefix } from '@/lib/uploads'
4346import {
@@ -338,13 +341,30 @@ export async function uploadWorkspaceFile(
338341 fileBuffer : Buffer ,
339342 fileName : string ,
340343 contentType : string ,
341- options ?: { folderId ?: string | null ; exactName ?: boolean }
344+ options ?: { folderId ?: string | null ; folderPath ?: string ; exactName ?: boolean }
342345) : Promise < UploadedWorkspaceFileRecord > {
343346 logger . info ( `Uploading workspace file: ${ fileName } for workspace ${ workspaceId } ` )
344347
345- const folderTarget = await resolveWorkspaceFileFolderTarget ( workspaceId , options ?. folderId )
346- const folderId = folderTarget ?. id ?? null
347- const folderPath = folderTarget ?. path ?? null
348+ if ( options ?. folderId !== undefined && options . folderPath !== undefined ) {
349+ throw new OrchestrationError ( 'validation' , 'Specify either folderId or folderPath, not both' )
350+ }
351+
352+ let folderId : string | null
353+ let folderPath : string | null
354+ if ( options ?. folderPath !== undefined ) {
355+ const folderPathSegments = parseFolderPath ( options . folderPath )
356+ const folderIndex = await loadActiveFolderPathIndex ( workspaceId , 'file' )
357+ const resolvedFolderId = resolveFolderPathFromIndex ( folderIndex , options . folderPath )
358+ if ( resolvedFolderId === undefined ) {
359+ throw new OrchestrationError ( 'not_found' , 'Target folder not found' )
360+ }
361+ folderId = resolvedFolderId
362+ folderPath = resolvedFolderId ? folderPathSegments . join ( '/' ) : null
363+ } else {
364+ const folderTarget = await resolveWorkspaceFileFolderTarget ( workspaceId , options ?. folderId )
365+ folderId = folderTarget ?. id ?? null
366+ folderPath = folderTarget ?. path ?? null
367+ }
348368 const normalizedFileName = normalizeWorkspaceFileItemName ( fileName , 'File' )
349369 const exactName = options ?. exactName ?? false
350370 const storageBillingContext = await resolveStorageBillingContext ( workspaceId )
@@ -370,7 +390,7 @@ export async function uploadWorkspaceFile(
370390 purpose : 'workspace' ,
371391 userId : userId ,
372392 workspaceId : workspaceId ,
373- ...( folderId ? { folderId } : { } ) ,
393+ ...( folderId && options ?. folderPath === undefined ? { folderId } : { } ) ,
374394 }
375395
376396 const uploadResult = await uploadFile ( {
@@ -392,12 +412,24 @@ export async function uploadWorkspaceFile(
392412 }
393413 try {
394414 finalized = await db . transaction ( async ( tx ) => {
415+ await acquireFolderMutationLock ( tx , workspaceId , 'file' )
416+ let activeFolderId : string | null
417+ if ( options ?. folderPath !== undefined ) {
418+ const folderIndex = await loadActiveFolderPathIndex ( workspaceId , 'file' , tx )
419+ const resolvedFolderId = resolveFolderPathFromIndex ( folderIndex , options . folderPath )
420+ if ( resolvedFolderId === undefined ) {
421+ throw new OrchestrationError ( 'not_found' , 'Target folder not found' )
422+ }
423+ activeFolderId = resolvedFolderId
424+ } else {
425+ activeFolderId = await assertWorkspaceFileFolderTarget ( workspaceId , folderId , tx )
426+ }
395427 const inserted = await insertWorkspaceFileMetadataInTx ( tx , {
396428 id : fileId ,
397429 key : uploadResult . key ,
398430 userId,
399431 workspaceId,
400- folderId,
432+ folderId : activeFolderId ,
401433 originalName : uniqueName ,
402434 contentType,
403435 size : fileBuffer . length ,
@@ -537,7 +569,7 @@ export async function registerUploadedWorkspaceFile(params: {
537569 }
538570 }
539571
540- const folderId = await assertWorkspaceFileFolderTarget ( workspaceId , params . folderId )
572+ const folderId = params . folderId ?? null
541573
542574 const storageBillingContext = await resolveStorageBillingContext ( workspaceId )
543575 for ( let attempt = 0 ; attempt < MAX_UPLOAD_UNIQUE_RETRIES ; attempt ++ ) {
@@ -549,12 +581,14 @@ export async function registerUploadedWorkspaceFile(params: {
549581 )
550582
551583 const finalized = await db . transaction ( async ( tx ) => {
584+ await acquireFolderMutationLock ( tx , workspaceId , 'file' )
585+ const activeFolderId = await assertWorkspaceFileFolderTarget ( workspaceId , folderId , tx )
552586 const inserted = await insertWorkspaceFileMetadataInTx ( tx , {
553587 id : fileId ,
554588 key,
555589 userId,
556590 workspaceId,
557- folderId,
591+ folderId : activeFolderId ,
558592 originalName : displayName ,
559593 contentType,
560594 size : verifiedSize ,
0 commit comments