11import { Request , Response } from "express" ;
22import { FileService , ReservedFileNameError } from "../services/FileService" ;
33import multer from "multer" ;
4+ import { v4 as uuidv4 } from "uuid" ;
45
56export const MAX_FILE_SIZE = 20 * 1024 * 1024 ; // 20MB limit
67
@@ -16,6 +17,80 @@ export class FileController {
1617 this . fileService = new FileService ( ) ;
1718 }
1819
20+ presignUpload = async ( req : Request , res : Response ) => {
21+ try {
22+ if ( ! req . user ) {
23+ return res . status ( 401 ) . json ( { error : "Authentication required" } ) ;
24+ }
25+
26+ const { filename, mimeType, size } = req . body ;
27+
28+ if ( ! filename || ! mimeType || ! size ) {
29+ return res . status ( 400 ) . json ( { error : "filename, mimeType, and size are required" } ) ;
30+ }
31+
32+ const fileId = uuidv4 ( ) ;
33+ const key = this . fileService . s3Service . generateKey ( req . user . id , fileId , filename ) ;
34+ const uploadUrl = await this . fileService . s3Service . generateUploadUrl ( key , mimeType ) ;
35+
36+ res . json ( { uploadUrl, key, fileId } ) ;
37+ } catch ( error ) {
38+ console . error ( "Error generating presigned URL:" , error ) ;
39+ res . status ( 500 ) . json ( { error : "Failed to generate upload URL" } ) ;
40+ }
41+ } ;
42+
43+ confirmUpload = async ( req : Request , res : Response ) => {
44+ try {
45+ if ( ! req . user ) {
46+ return res . status ( 401 ) . json ( { error : "Authentication required" } ) ;
47+ }
48+
49+ const { key, fileId, filename, mimeType, size, displayName, description } = req . body ;
50+
51+ if ( ! key || ! fileId || ! filename || ! mimeType || ! size ) {
52+ return res . status ( 400 ) . json ( { error : "key, fileId, filename, mimeType, and size are required" } ) ;
53+ }
54+
55+ const head = await this . fileService . s3Service . headObject ( key ) ;
56+ const md5Hash = head . etag ;
57+ const url = this . fileService . s3Service . getPublicUrl ( key ) ;
58+
59+ const file = await this . fileService . createFileWithUrl (
60+ fileId ,
61+ filename ,
62+ mimeType ,
63+ size ,
64+ md5Hash ,
65+ url ,
66+ req . user . id ,
67+ displayName ,
68+ description ,
69+ ) ;
70+
71+ res . status ( 201 ) . json ( {
72+ id : file . id ,
73+ name : file . name ,
74+ displayName : file . displayName ,
75+ description : file . description ,
76+ mimeType : file . mimeType ,
77+ size : file . size ,
78+ md5Hash : file . md5Hash ,
79+ url : file . url ,
80+ createdAt : file . createdAt ,
81+ } ) ;
82+ } catch ( error ) {
83+ console . error ( "Error confirming upload:" , error ) ;
84+ if ( error instanceof ReservedFileNameError ) {
85+ return res . status ( 400 ) . json ( { error : error . message } ) ;
86+ }
87+ if ( error instanceof Error ) {
88+ return res . status ( 400 ) . json ( { error : error . message } ) ;
89+ }
90+ res . status ( 500 ) . json ( { error : "Failed to confirm upload" } ) ;
91+ }
92+ } ;
93+
1994 uploadFile = [
2095 upload . single ( 'file' ) ,
2196 async ( req : Request , res : Response ) => {
@@ -97,6 +172,7 @@ export class FileController {
97172 mimeType : file . mimeType ,
98173 size : file . size ,
99174 md5Hash : file . md5Hash ,
175+ url : file . url ,
100176 ownerId : file . ownerId ,
101177 createdAt : file . createdAt ,
102178 updatedAt : file . updatedAt ,
@@ -158,6 +234,11 @@ export class FileController {
158234 return res . status ( 404 ) . json ( { error : "File not found" } ) ;
159235 }
160236
237+ if ( file . url ) {
238+ return res . redirect ( file . url ) ;
239+ }
240+
241+ // Legacy fallback for files still in DB
161242 res . setHeader ( 'Content-Type' , file . mimeType ) ;
162243 res . setHeader ( 'Content-Disposition' , `attachment; filename="${ file . name } "` ) ;
163244 res . setHeader ( 'Content-Length' , file . size . toString ( ) ) ;
0 commit comments