88using Microsoft . Net . Http . Headers ;
99using System . Globalization ;
1010using FwHeadless . Media ;
11+ using LexCore . Exceptions ;
1112using MimeMapping ;
1213
1314namespace FwHeadless . Controllers ;
@@ -68,16 +69,12 @@ public static async Task<Results<PhysicalFileHttpResult, NotFound>> GetFile(
6869 public static async Task < Results < Ok , NotFound > > DeleteFile (
6970 Guid fileId ,
7071 IOptions < FwHeadlessConfig > config ,
72+ MediaFileService mediaFileService ,
7173 LexBoxDbContext lexBoxDb )
7274 {
73- var mediaFile = await lexBoxDb . Files . FindAsync ( fileId ) ;
75+ var mediaFile = await mediaFileService . FindMediaFileAsync ( fileId ) ;
7476 if ( mediaFile is null ) return TypedResults . NotFound ( ) ;
75- var projectId = mediaFile . ProjectId ;
76- var project = await lexBoxDb . Projects . FindAsync ( projectId ) ;
77- if ( project is null ) return TypedResults . NotFound ( ) ;
78- var projectFolder = config . Value . GetFwDataProject ( project . Code , projectId ) . ProjectFolder ;
79- SafeDeleteMediaFile ( mediaFile , projectFolder , lexBoxDb ) ;
80- await lexBoxDb . SaveChangesAsync ( ) ;
77+ await mediaFileService . DeleteMediaFile ( mediaFile ) ;
8178 return TypedResults . Ok ( ) ;
8279 }
8380
@@ -90,9 +87,10 @@ public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, No
9087 [ FromForm ] string ? linkedFilesSubfolderOverride ,
9188 HttpContext httpContext ,
9289 IOptions < FwHeadlessConfig > config ,
90+ MediaFileService mediaFileService ,
9391 LexBoxDbContext lexBoxDb )
9492 {
95- var result = await HandleFileUpload ( fileId , projectId , filename , file , metadata , linkedFilesSubfolderOverride , httpContext , config , lexBoxDb , newFilesAllowed : false ) ;
93+ var result = await HandleFileUpload ( fileId , projectId , filename , file , metadata , linkedFilesSubfolderOverride , httpContext , config , lexBoxDb , mediaFileService , newFilesAllowed : false ) ;
9694 return result ;
9795 }
9896
@@ -106,23 +104,26 @@ public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, No
106104 [ FromForm ] string ? linkedFilesSubfolderOverride ,
107105 HttpContext httpContext ,
108106 IOptions < FwHeadlessConfig > config ,
107+ MediaFileService mediaFileService ,
109108 LexBoxDbContext lexBoxDb )
110109 {
111- var result = await HandleFileUpload ( fileId , projectId , filename , file , metadata , linkedFilesSubfolderOverride , httpContext , config , lexBoxDb , newFilesAllowed : true ) ;
110+ var result = await HandleFileUpload ( fileId , projectId , filename , file , metadata , linkedFilesSubfolderOverride , httpContext , config , lexBoxDb , mediaFileService , newFilesAllowed : true ) ;
112111 return result ;
113112 }
114113
115- public static async Task < Results < Ok < PostFileResult > , Created < PostFileResult > , NotFound , BadRequest < FileUploadErrorMessage > , ProblemHttpResult > > HandleFileUpload (
116- Guid ? fileId ,
117- Guid projectId ,
118- string ? filename ,
119- IFormFile file ,
120- FileMetadata ? metadata ,
121- string ? linkedFilesSubfolderOverride ,
122- HttpContext httpContext ,
123- IOptions < FwHeadlessConfig > config ,
124- LexBoxDbContext lexBoxDb ,
125- bool newFilesAllowed )
114+ public static async
115+ Task < Results < Ok < PostFileResult > , Created < PostFileResult > , NotFound , BadRequest < FileUploadErrorMessage > ,
116+ ProblemHttpResult > > HandleFileUpload ( Guid ? fileId ,
117+ Guid projectId ,
118+ string ? filename ,
119+ IFormFile file ,
120+ FileMetadata ? metadata ,
121+ string ? linkedFilesSubfolderOverride ,
122+ HttpContext httpContext ,
123+ IOptions < FwHeadlessConfig > config ,
124+ LexBoxDbContext lexBoxDb ,
125+ MediaFileService mediaFileService ,
126+ bool newFilesAllowed )
126127 {
127128 if ( CheckUploadSize ( file , httpContext , config ) is { } result ) return result ;
128129 MediaFile ? mediaFile ;
@@ -138,7 +139,7 @@ public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, No
138139 linkedFilesSubfolderOverride ,
139140 newFilesAllowed ,
140141 httpContext ,
141- config ) ;
142+ mediaFileService ) ;
142143 }
143144 catch ( NotFoundException ) { return TypedResults . NotFound ( ) ; }
144145 catch ( UploadedFilesCannotBeMovedToNewProjects ) { return TypedResults . BadRequest ( FileUploadErrorMessage . UploadedFilesCannotBeMovedToNewProjects ) ; }
@@ -178,54 +179,6 @@ public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, No
178179 return null ;
179180 }
180181
181- private static async Task < long > WriteFileToDisk ( string filePath , Stream contents )
182- {
183- if ( contents is null ) return 0 ;
184- long startPosition = 0 ;
185- try
186- {
187- startPosition = contents . Position ;
188- }
189- catch { }
190- // First write to temp file, then move file into place, overwriting existing file
191- // That way files will be replaced atomically, and a failure halfway through the process won't result in the existing file being lost
192- string tempFile = "" ;
193- try
194- {
195- var dirName = Path . GetDirectoryName ( filePath ) ;
196- if ( dirName is not null ) Directory . CreateDirectory ( dirName ) ;
197- tempFile = Path . Join ( dirName , Path . GetRandomFileName ( ) ) ;
198- await using ( var writeStream = File . Open ( tempFile , FileMode . CreateNew , FileAccess . Write , FileShare . ReadWrite ) )
199- {
200- await contents . CopyToAsync ( writeStream ) ;
201- }
202- File . Move ( tempFile , filePath , overwrite : true ) ;
203- }
204- finally
205- {
206- // If anything fails, delete temp file
207- if ( ! string . IsNullOrEmpty ( tempFile ) && File . Exists ( tempFile ) ) SafeDelete ( tempFile ) ;
208- }
209- long endPosition = 0 ;
210- try
211- {
212- endPosition = contents . Position ;
213- }
214- catch { }
215- var calcLength = endPosition - startPosition ;
216- if ( calcLength == 0 )
217- {
218- // Either the stream was empty, or its Position attribute wasn't reliable, so we need
219- // to look at the file we just wrote to determine the size
220- var fileInfo = new FileInfo ( filePath ) ;
221- return fileInfo . Length ;
222- }
223- else
224- {
225- return calcLength ;
226- }
227- }
228-
229182 private static async Task AddEntityTagMetadata ( MediaFile mediaFile , string filePath )
230183 {
231184 mediaFile . InitializeMetadataIfNeeded ( filePath ) ;
@@ -238,7 +191,8 @@ private static async Task<bool> AddEntityTagMetadataIfNotPresent(MediaFile media
238191 {
239192 if ( mediaFile . Metadata ? . Sha256Hash is null )
240193 {
241- await AddEntityTagMetadata ( mediaFile , filePath ) ;
194+ mediaFile . InitializeMetadataIfNeeded ( filePath ) ;
195+ mediaFile . Metadata . Sha256Hash = await MediaFileService . Sha256OfFile ( filePath ) ;
242196 return true ;
243197 }
244198 return false ;
@@ -284,13 +238,7 @@ private static async Task<FileMetadata> InitMetadata(FileMetadata? metadata, IFo
284238 return metadata ;
285239 }
286240
287- private class NotFoundException : Exception ;
288- private class UploadedFilesCannotBeMovedToNewProjects : Exception ;
289- private class UploadedFilesCannotBeMovedToDifferentLinkedFilesSubfolders : Exception ;
290- private class ProjectFolderNotFoundInFwHeadless : Exception ;
291- private class FileTooLarge : Exception ;
292- private static async Task< ( MediaFile , bool newFile ) > CreateOrUpdateMediaFile(
293- LexBoxDbContext lexBoxDb ,
241+ private static async Task < ( MediaFile , bool newFile ) > CreateOrUpdateMediaFile ( LexBoxDbContext lexBoxDb ,
294242 Guid ? fileId ,
295243 Guid projectId ,
296244 string ? filename ,
@@ -299,7 +247,7 @@ private class FileTooLarge : Exception;
299247 string ? subfolderOverride ,
300248 bool newFilesAllowed ,
301249 HttpContext httpContext ,
302- IOptions < FwHeadlessConfig > config )
250+ MediaFileService mediaFileService )
303251 {
304252 if ( fileId is null || fileId . Value == default )
305253 {
@@ -310,7 +258,7 @@ private class FileTooLarge : Exception;
310258 if ( mediaFile is null && ! newFilesAllowed )
311259 {
312260 // PUT requests must modify an existing file and return 404 if it doesn't exist
313- throw new NotFoundException ( ) ;
261+ throw NotFoundException . ForType < MediaFile > ( ) ;
314262 }
315263
316264 // If no filename specified in form, get it from uploaded file
@@ -343,40 +291,17 @@ private class FileTooLarge : Exception;
343291 }
344292
345293 var project = await lexBoxDb . Projects . FindAsync ( projectId ) ;
346- if ( project is null) throw new NotFoundException ( ) ;
347- var projectFolder = config . Value . GetFwDataProject ( project . Code , projectId ) . ProjectFolder ;
348- await WriteFileAndUpdateMediaFileMetadata ( lexBoxDb , mediaFile , projectFolder , file , config . Value . MaxUploadFileSizeBytes ) ;
294+ if ( project is null ) throw NotFoundException . ForType < Project > ( ) ;
295+ await SaveMediaFile ( mediaFileService , mediaFile , file ) ;
349296 return ( mediaFile , newFile ) ;
350297 }
351298
352- private static async Task WriteFileAndUpdateMediaFileMetadata ( LexBoxDbContext lexBoxDb , MediaFile mediaFile , string projectFolder , IFormFile file , long maxUploadSize )
299+ private static async Task SaveMediaFile ( MediaFileService mediaFileService ,
300+ MediaFile mediaFile ,
301+ IFormFile file )
353302 {
354- if ( ! Directory . Exists ( projectFolder ) )
355- {
356- throw new ProjectFolderNotFoundInFwHeadless ( ) ;
357- }
358-
359- var filePath = Path . Join ( projectFolder , mediaFile . Filename ) ;
360- if ( mediaFile . Metadata is not null ) mediaFile . Metadata . SizeInBytes = ( int ) file . Length ;
361- long writtenLength = 0 ;
362- await using ( var readStream = file . OpenReadStream ( ) )
363- {
364- writtenLength = await WriteFileToDisk ( filePath , readStream ) ;
365- }
366- if ( writtenLength > maxUploadSize )
367- {
368- SafeDeleteMediaFile ( mediaFile , projectFolder , lexBoxDb ) ;
369- await lexBoxDb . SaveChangesAsync ( ) ;
370- throw new FileTooLarge ( ) ;
371- }
372- if ( writtenLength != file . Length )
373- {
374- // TODO: Log warning about mismatched length?
375- if ( mediaFile . Metadata is not null ) mediaFile . Metadata . SizeInBytes = ( int ) writtenLength ;
376- }
377- await AddEntityTagMetadata ( mediaFile , filePath ) ;
378- mediaFile . UpdateUpdatedDate ( ) ;
379- await lexBoxDb . SaveChangesAsync ( ) ;
303+ await using var readStream = file . OpenReadStream ( ) ;
304+ await mediaFileService . SaveMediaFile ( mediaFile , readStream ) ;
380305 }
381306
382307 private static string ? GuessSubfolderFromMimeType ( string ? mimeType )
@@ -389,27 +314,4 @@ private static async Task WriteFileAndUpdateMediaFileMetadata(LexBoxDbContext le
389314 if ( mimeType == "application/mp4" ) return "AudioVisual" ; // Some apps don't want to commit to audio/ or video/, but we don't care which it is
390315 return null ;
391316 }
392-
393- private static void SafeDelete ( string filePath )
394- {
395- // Delete file at path, ignoring all errors such as "file not found"
396- try { File . Delete ( filePath ) ; }
397- catch { }
398- }
399-
400- private static void SafeDeleteDirectory ( string dirPath , bool recursive = false )
401- {
402- // Delete file at path, ignoring all errors such as "directory not empty"
403- try { Directory . Delete ( dirPath , recursive ) ; }
404- catch { }
405- }
406-
407- private static void SafeDeleteMediaFile ( MediaFile mediaFile , string projectFolder , LexBoxDbContext lexBoxDb )
408- {
409- var filePath = Path . Join ( projectFolder , mediaFile . Filename ) ;
410- SafeDelete ( filePath ) ;
411- var dirPath = Path . Join ( projectFolder , mediaFile . Id . ToString ( ) ) ;
412- SafeDeleteDirectory ( dirPath ) ; // Will not delete dir if not empty, but that's OK
413- lexBoxDb . Files . Remove ( mediaFile ) ;
414- }
415317}
0 commit comments