@@ -11,9 +11,60 @@ import { media, users } from "@/lib/db/schema";
1111import { logger } from "@/lib/logger" ;
1212import { convertHeicToJpeg } from "@/lib/media/heic" ;
1313import { S3_BUCKET_NAME , s3Client } from "@/lib/media/s3" ;
14+ import {
15+ generateAndUploadThumbnail ,
16+ getThumbnailS3Key ,
17+ } from "@/lib/media/thumbnail" ;
1418import { can } from "@/lib/policy" ;
1519import { contentDispositionFilename } from "@/lib/safe-filename" ;
1620
21+ async function streamToBuffer ( stream : AsyncIterable < Uint8Array > ) {
22+ const chunks : Uint8Array [ ] = [ ] ;
23+ for await ( const chunk of stream ) {
24+ chunks . push ( chunk ) ;
25+ }
26+ return Buffer . concat ( chunks ) ;
27+ }
28+
29+ async function generateMissingImageThumbnail (
30+ mediaItem : typeof media . $inferSelect ,
31+ ) {
32+ if ( ! mediaItem . mimeType . startsWith ( "image/" ) ) return null ;
33+ try {
34+ const source = await s3Client . send (
35+ new GetObjectCommand ( {
36+ Bucket : S3_BUCKET_NAME ,
37+ Key : mediaItem . s3Key ,
38+ } ) ,
39+ ) ;
40+ if ( ! source . Body ) throw new Error ( "S3 object had no body" ) ;
41+ const buffer = await streamToBuffer (
42+ source . Body as AsyncIterable < Uint8Array > ,
43+ ) ;
44+ const thumbnailS3Key = await generateAndUploadThumbnail (
45+ buffer ,
46+ mediaItem . mimeType ,
47+ mediaItem . id ,
48+ undefined ,
49+ { uploadedBy : mediaItem . uploadedById , eventId : mediaItem . eventId } ,
50+ ) ;
51+ if ( ! thumbnailS3Key ) return null ;
52+ if ( thumbnailS3Key !== mediaItem . thumbnailS3Key ) {
53+ await db
54+ . update ( media )
55+ . set ( { thumbnailS3Key } )
56+ . where ( eq ( media . id , mediaItem . id ) ) ;
57+ }
58+ return thumbnailS3Key ;
59+ } catch ( error ) {
60+ logger . error (
61+ { mediaId : mediaItem . id , error } ,
62+ "thumbnail generation failed" ,
63+ ) ;
64+ return null ;
65+ }
66+ }
67+
1768export async function GET (
1869 request : NextRequest ,
1970 {
@@ -126,13 +177,13 @@ export async function GET(
126177 let s3Key = mediaItem . s3Key ;
127178 let filename = contentDispositionFilename ( mediaItem . filename ) ;
128179 if ( variant === "thumbnail" ) {
180+ const baseName =
181+ filename . substring ( 0 , filename . lastIndexOf ( "." ) ) || filename ;
182+ filename = `thumbnail_${ baseName } .jpg` ;
129183 if ( mediaItem . thumbnailS3Key ) {
130184 s3Key = mediaItem . thumbnailS3Key ;
131- const baseName =
132- filename . substring ( 0 , filename . lastIndexOf ( "." ) ) || filename ;
133- filename = `thumbnail_${ baseName } .jpg` ;
134- } else {
135- s3Key = mediaItem . s3Key ;
185+ } else if ( mediaItem . mimeType . startsWith ( "image/" ) ) {
186+ s3Key = getThumbnailS3Key ( mediaItem . id ) ;
136187 }
137188 }
138189 const command = new GetObjectCommand ( {
@@ -147,15 +198,62 @@ export async function GET(
147198 let s3Response : GetObjectCommandOutput ;
148199 try {
149200 s3Response = await s3Client . send ( command ) ;
201+ if (
202+ variant === "thumbnail" &&
203+ mediaItem . mimeType . startsWith ( "image/" ) &&
204+ ! mediaItem . thumbnailS3Key &&
205+ s3Key === getThumbnailS3Key ( mediaItem . id )
206+ ) {
207+ await db
208+ . update ( media )
209+ . set ( { thumbnailS3Key : s3Key } )
210+ . where ( eq ( media . id , mediaItem . id ) ) ;
211+ }
150212 } catch ( error : any ) {
151213 if ( error ?. $metadata ?. httpStatusCode === 304 ) {
152214 return new NextResponse ( null , { status : 304 } ) ;
153215 }
154- logger . error ( `Failed to fetch from S3:` , error ) ;
155- return new NextResponse ( "Failed to fetch media" , { status : 502 } ) ;
216+ if ( variant === "thumbnail" && mediaItem . mimeType . startsWith ( "image/" ) ) {
217+ const regeneratedThumbnailKey =
218+ await generateMissingImageThumbnail ( mediaItem ) ;
219+ if ( regeneratedThumbnailKey ) {
220+ try {
221+ s3Response = await s3Client . send (
222+ new GetObjectCommand ( {
223+ Bucket : S3_BUCKET_NAME ,
224+ Key : regeneratedThumbnailKey ,
225+ Range : requestRange ,
226+ IfNoneMatch : request . headers . get ( "if-none-match" ) ?? undefined ,
227+ IfModifiedSince : request . headers . get ( "if-modified-since" )
228+ ? new Date ( request . headers . get ( "if-modified-since" ) as string )
229+ : undefined ,
230+ } ) ,
231+ ) ;
232+ } catch ( retryError : any ) {
233+ if ( retryError ?. $metadata ?. httpStatusCode === 304 ) {
234+ return new NextResponse ( null , { status : 304 } ) ;
235+ }
236+ logger . error (
237+ "Failed to fetch regenerated thumbnail from S3:" ,
238+ retryError ,
239+ ) ;
240+ return new NextResponse ( "Failed to fetch media" , { status : 502 } ) ;
241+ }
242+ } else {
243+ logger . error ( `Failed to fetch from S3:` , error ) ;
244+ return new NextResponse ( "Failed to fetch media" , { status : 502 } ) ;
245+ }
246+ } else {
247+ logger . error ( `Failed to fetch from S3:` , error ) ;
248+ return new NextResponse ( "Failed to fetch media" , { status : 502 } ) ;
249+ }
156250 }
157251 const headers = new Headers ( ) ;
158- headers . set ( "Content-Type" , s3Response . ContentType || mediaItem . mimeType ) ;
252+ headers . set (
253+ "Content-Type" ,
254+ s3Response . ContentType ||
255+ ( variant === "thumbnail" ? "image/jpeg" : mediaItem . mimeType ) ,
256+ ) ;
159257 headers . set ( "X-Content-Type-Options" , "nosniff" ) ;
160258 if ( s3Response . ContentLength ) {
161259 headers . set ( "Content-Length" , String ( s3Response . ContentLength ) ) ;
0 commit comments