@@ -19,6 +19,7 @@ import {
1919 uploadFile ,
2020} from '@/lib/uploads/core/storage-service'
2121import { getFileMetadataByKey , insertFileMetadata } from '@/lib/uploads/server/metadata'
22+ import { getPostgresErrorCode } from '@/lib/core/utils/pg-error'
2223import { generateRestoreName } from '@/lib/core/utils/restore-name'
2324import { isUuid , sanitizeFileName } from '@/executor/constants'
2425import type { UserFile } from '@/executor/types'
@@ -110,7 +111,7 @@ export async function uploadWorkspaceFile(
110111
111112 const exists = await fileExistsInWorkspace ( workspaceId , fileName )
112113 if ( exists ) {
113- throw new Error ( `A file named " ${ fileName } " already exists in this workspace` )
114+ throw new FileConflictError ( fileName )
114115 }
115116
116117 const quotaCheck = await checkStorageQuota ( userId , fileBuffer . length )
@@ -204,6 +205,12 @@ export async function uploadWorkspaceFile(
204205 context : 'workspace' ,
205206 }
206207 } catch ( error ) {
208+ if ( error instanceof FileConflictError ) {
209+ throw error
210+ }
211+ if ( getPostgresErrorCode ( error ) === '23505' ) {
212+ throw new FileConflictError ( fileName )
213+ }
207214 logger . error ( `Failed to upload workspace file ${ fileName } :` , error )
208215 throw new Error (
209216 `Failed to upload file: ${ error instanceof Error ? error . message : 'Unknown error' } `
@@ -573,17 +580,25 @@ export async function renameWorkspaceFile(
573580 throw new FileConflictError ( trimmedName )
574581 }
575582
576- const updated = await db
577- . update ( workspaceFiles )
578- . set ( { originalName : trimmedName } )
579- . where (
580- and (
581- eq ( workspaceFiles . id , fileId ) ,
582- eq ( workspaceFiles . workspaceId , workspaceId ) ,
583- eq ( workspaceFiles . context , 'workspace' )
583+ let updated : { id : string } [ ]
584+ try {
585+ updated = await db
586+ . update ( workspaceFiles )
587+ . set ( { originalName : trimmedName } )
588+ . where (
589+ and (
590+ eq ( workspaceFiles . id , fileId ) ,
591+ eq ( workspaceFiles . workspaceId , workspaceId ) ,
592+ eq ( workspaceFiles . context , 'workspace' )
593+ )
584594 )
585- )
586- . returning ( { id : workspaceFiles . id } )
595+ . returning ( { id : workspaceFiles . id } )
596+ } catch ( error : unknown ) {
597+ if ( getPostgresErrorCode ( error ) === '23505' ) {
598+ throw new FileConflictError ( trimmedName )
599+ }
600+ throw error
601+ }
587602
588603 if ( updated . length === 0 ) {
589604 throw new Error ( 'File not found or could not be renamed' )
@@ -651,22 +666,43 @@ export async function restoreWorkspaceFile(workspaceId: string, fileId: string):
651666 throw new Error ( 'Cannot restore file into an archived workspace' )
652667 }
653668
654- const newName = await generateRestoreName (
655- fileRecord . name ,
656- ( candidate ) => fileExistsInWorkspace ( workspaceId , candidate ) ,
657- { hasExtension : true }
658- )
669+ /**
670+ * A concurrent upload/rename can claim the chosen name after `generateRestoreName`'s check (MVCC).
671+ * Retries pick a new random suffix; 23505 maps to {@link FileConflictError} after exhaustion.
672+ */
673+ const maxUniqueViolationRetries = 8
674+ let attemptedRestoreName = ''
659675
660- await db
661- . update ( workspaceFiles )
662- . set ( { deletedAt : null , originalName : newName } )
663- . where (
664- and (
665- eq ( workspaceFiles . id , fileId ) ,
666- eq ( workspaceFiles . workspaceId , workspaceId ) ,
667- eq ( workspaceFiles . context , 'workspace' )
676+ for ( let attempt = 0 ; attempt < maxUniqueViolationRetries ; attempt ++ ) {
677+ attemptedRestoreName = ''
678+ try {
679+ const newName = await generateRestoreName (
680+ fileRecord . name ,
681+ ( candidate ) => fileExistsInWorkspace ( workspaceId , candidate ) ,
682+ { hasExtension : true }
668683 )
669- )
684+ attemptedRestoreName = newName
685+
686+ await db
687+ . update ( workspaceFiles )
688+ . set ( { deletedAt : null , originalName : newName } )
689+ . where (
690+ and (
691+ eq ( workspaceFiles . id , fileId ) ,
692+ eq ( workspaceFiles . workspaceId , workspaceId ) ,
693+ eq ( workspaceFiles . context , 'workspace' )
694+ )
695+ )
670696
671- logger . info ( `Successfully restored workspace file: ${ newName } ` )
697+ logger . info ( `Successfully restored workspace file: ${ newName } ` )
698+ return
699+ } catch ( error : unknown ) {
700+ if ( getPostgresErrorCode ( error ) !== '23505' ) {
701+ throw error
702+ }
703+ if ( attempt === maxUniqueViolationRetries - 1 ) {
704+ throw new FileConflictError ( attemptedRestoreName || fileRecord . name )
705+ }
706+ }
707+ }
672708}
0 commit comments