@@ -21,32 +21,6 @@ export const getAfterChangeHook =
2121 const files = getIncomingFiles ( { data : doc , req } )
2222
2323 if ( files . length > 0 ) {
24- // If there is a previous doc, files and the operation is update,
25- // delete the old files before uploading the new ones.
26- if ( previousDoc && operation === 'update' ) {
27- let filesToDelete : string [ ] = [ ]
28-
29- if ( typeof previousDoc ?. filename === 'string' ) {
30- filesToDelete . push ( previousDoc . filename )
31- }
32-
33- if ( typeof previousDoc . sizes === 'object' ) {
34- filesToDelete = filesToDelete . concat (
35- Object . values ( previousDoc ?. sizes || [ ] ) . map (
36- ( resizedFileData ) => resizedFileData ?. filename as string ,
37- ) ,
38- )
39- }
40-
41- const deletionPromises = filesToDelete . map ( async ( filename ) => {
42- if ( filename ) {
43- await adapter . handleDelete ( { collection, doc : previousDoc , filename, req } )
44- }
45- } )
46-
47- await Promise . all ( deletionPromises )
48- }
49-
5024 const uploadResults = await Promise . all (
5125 files
5226 . filter ( ( file ) => ! file . clientUploadContext )
@@ -71,31 +45,78 @@ export const getAfterChangeHook =
7145 { } as Partial < FileData & TypeWithID > ,
7246 )
7347
48+ let docWithMetadata = doc
49+
7450 if ( Object . keys ( uploadMetadata ) . length > 0 ) {
75- try {
76- if ( ! req . context ) {
77- req . context = { }
78- }
79- req . context . skipCloudStorage = true
51+ if ( ! req . context ) {
52+ req . context = { }
53+ }
54+ req . context . skipCloudStorage = true
8055
81- // Clear to prevent re-processing
82- req . file = undefined
83- req . payloadUploadSizes = undefined
56+ // Clear to prevent re-processing
57+ req . file = undefined
58+ req . payloadUploadSizes = undefined
8459
60+ try {
8561 await req . payload . update ( {
8662 id : doc . id ,
8763 collection : collection . slug ,
8864 data : uploadMetadata ,
8965 depth : 0 ,
9066 req,
9167 } )
68+ } finally {
9269 delete req . context . skipCloudStorage
93- return { ...doc , ...uploadMetadata }
94- } catch ( updateError : unknown ) {
95- req . payload . logger . warn (
96- `Failed to persist upload data for collection ${ collection . slug } document ${ doc . id } : ${ String ( updateError ) } ` ,
70+ }
71+
72+ docWithMetadata = { ...doc , ...uploadMetadata }
73+ }
74+
75+ // Delete previous files only after the new upload and metadata
76+ // persistence have succeeded. Deleting earlier would orphan the
77+ // record if a later step throws (e.g. a user-defined afterChange
78+ // hook on the same collection).
79+ if ( previousDoc && operation === 'update' ) {
80+ let filesToDelete : string [ ] = [ ]
81+
82+ if ( typeof previousDoc ?. filename === 'string' ) {
83+ filesToDelete . push ( previousDoc . filename )
84+ }
85+
86+ if ( typeof previousDoc . sizes === 'object' ) {
87+ filesToDelete = filesToDelete . concat (
88+ Object . values ( previousDoc ?. sizes || [ ] ) . map (
89+ ( resizedFileData ) => resizedFileData ?. filename as string ,
90+ ) ,
9791 )
9892 }
93+
94+ // Collect new filenames (main + sizes) so we don't delete a
95+ // file that the new upload reused (e.g. same filename on reupload
96+ // where Payload overwrites in place).
97+ const newFilenames = new Set < string > ( )
98+ if ( typeof docWithMetadata . filename === 'string' ) {
99+ newFilenames . add ( docWithMetadata . filename )
100+ }
101+ if ( typeof docWithMetadata . sizes === 'object' ) {
102+ for ( const size of Object . values ( docWithMetadata . sizes || { } ) ) {
103+ if ( size ?. filename && typeof size . filename === 'string' ) {
104+ newFilenames . add ( size . filename )
105+ }
106+ }
107+ }
108+
109+ const deletionPromises = filesToDelete . map ( async ( filename ) => {
110+ if ( filename && ! newFilenames . has ( filename ) ) {
111+ await adapter . handleDelete ( { collection, doc : previousDoc , filename, req } )
112+ }
113+ } )
114+
115+ await Promise . all ( deletionPromises )
116+ }
117+
118+ if ( docWithMetadata !== doc ) {
119+ return docWithMetadata
99120 }
100121 }
101122 } catch ( err : unknown ) {
0 commit comments