feat(backend): implement on-demand thumbnail generation
Some checks failed
Build and Publish Docker Image / deploy (push) Failing after 44s

When a thumbnail is requested but not yet available, attempt to generate
it synchronously on-demand instead of immediately falling back to the
placeholder image.

- Export thumbnail generation helpers from the jobs package.
- Update the Thumbnail handler to trigger on-demand generation if the
  thumbnail object is missing.
- Save the updated box metadata with the new thumbnail reference.
- Fall back to the placeholder image only if on-demand generation fails.
This commit is contained in:
2026-06-03 15:20:26 +03:00
parent 3b278642dc
commit eff831b142
2 changed files with 44 additions and 0 deletions

View File

@@ -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.