diff --git a/backend/libs/handlers/download.go b/backend/libs/handlers/download.go index a4a654f..95ba608 100644 --- a/backend/libs/handlers/download.go +++ b/backend/libs/handlers/download.go @@ -15,6 +15,7 @@ import ( "time" "warpbox.dev/backend/libs/helpers" + "warpbox.dev/backend/libs/jobs" "warpbox.dev/backend/libs/services" "warpbox.dev/backend/libs/web" ) @@ -319,6 +320,17 @@ func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) { object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file) if err != nil { + if thumbnail := a.generateMissingThumbnailForRequest(r, box, file); thumbnail != "" { + file.Thumbnail = thumbnail + object, err = a.uploadService.OpenThumbnailObject(r.Context(), box, file) + if err == nil { + defer object.Body.Close() + w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("Cache-Control", "public, max-age=604800, immutable") + http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body)) + return + } + } // The thumbnail isn't generated yet (background job pending). Serve the // placeholder but mark it non-cacheable, otherwise the browser would // keep showing the placeholder until a hard refresh once the real @@ -333,6 +345,30 @@ func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body)) } +func (a *App) generateMissingThumbnailForRequest(r *http.Request, box services.Box, file services.File) string { + if file.Thumbnail != "" || !jobs.NeedsThumbnail(file) { + return "" + } + thumbnail, err := jobs.GenerateThumbnailForFile(a.uploadService, box, file) + if err != nil || thumbnail == "" { + if err != nil { + a.logger.Warn("on-demand thumbnail generation failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4102, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) + } + return "" + } + for i := range box.Files { + if box.Files[i].ID == file.ID { + box.Files[i].Thumbnail = thumbnail + break + } + } + if err := a.uploadService.SaveBox(box); err != nil { + a.logger.Warn("on-demand thumbnail metadata save failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4103, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) + return "" + } + return thumbnail +} + // servePlaceholderThumbnail serves the fallback image with no-store so the // browser re-requests on the next load and picks up the real thumbnail as soon // as it has been generated. diff --git a/backend/libs/jobs/thumbnails.go b/backend/libs/jobs/thumbnails.go index 9436b2a..0da9021 100644 --- a/backend/libs/jobs/thumbnails.go +++ b/backend/libs/jobs/thumbnails.go @@ -141,6 +141,14 @@ func needsThumbnail(file services.File) bool { return file.PreviewKind == "image" || file.PreviewKind == "video" || isTextThumbnailCandidate(file) } +func NeedsThumbnail(file services.File) bool { + return needsThumbnail(file) +} + +func GenerateThumbnailForFile(uploadService *services.UploadService, box services.Box, file services.File) (string, error) { + return generateThumbnail(uploadService, box, file) +} + func generateThumbnail(uploadService *services.UploadService, box services.Box, file services.File) (string, error) { thumbnailName := "@thumb@" + file.ID + ".jpg" object, err := uploadService.OpenFileObject(context.Background(), box, file)