Extract public file serving into filemanager

Move range, ETag, and cache handling out of the files API handler
so brand logos and other public assets can reuse the same code path.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-15 17:32:58 +02:00
parent 48dba254ca
commit 01acda1d84
2 changed files with 113 additions and 79 deletions

View File

@@ -23,9 +23,7 @@ package files_v1
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
@@ -111,90 +109,20 @@ func (h *Handler) handleGetPublicFile(w http.ResponseWriter, r *http.Request) {
return
}
file, err := h.fileSvc.GetPublicFile(r.Context(), fileID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
jsonx.RenderNotFound(w, fmt.Errorf("file not found"))
return
}
err = h.fileSvc.ServePublicFile(r.Context(), w, r, fileID)
if errors.Is(err, filemanager.ErrPublicFileNotFound) {
jsonx.RenderNotFound(w, fmt.Errorf("file not found"))
return
}
if err != nil {
h.logger.ErrorCtx(
r.Context(),
"cannot get public file URL",
"cannot serve public file",
log.Error(err),
log.String("file_id", fileIDStr),
)
jsonx.RenderInternalServerError(w)
return
}
conds := filemanager.FileConditions{
IfNoneMatch: r.Header.Get("If-None-Match"),
IfRange: r.Header.Get("If-Range"),
Range: r.Header.Get("Range"),
}
if ifModifiedSince := r.Header.Get("If-Modified-Since"); ifModifiedSince != "" {
if t, parseErr := http.ParseTime(ifModifiedSince); parseErr == nil {
conds.IfModifiedSince = t
}
}
obj, err := h.fileSvc.OpenFile(r.Context(), file, conds)
if err != nil {
h.logger.ErrorCtx(
r.Context(),
"cannot open public file",
log.Error(err),
log.String("file_id", fileIDStr),
)
jsonx.RenderInternalServerError(w)
return
}
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Header().Set("Accept-Ranges", "bytes")
if obj.ETag != "" {
w.Header().Set("ETag", obj.ETag)
}
if obj.NotModified {
w.WriteHeader(http.StatusNotModified)
return
}
if obj.RangeNotSatisfiable {
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", file.FileSize))
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
defer func() { _ = obj.Body.Close() }()
w.Header().Set("Content-Type", file.MimeType)
w.Header().Set("Content-Length", strconv.FormatInt(obj.ContentLength, 10))
if !obj.LastModified.IsZero() {
w.Header().Set("Last-Modified", obj.LastModified.UTC().Format(http.TimeFormat))
}
if obj.PartialContent {
w.Header().Set("Content-Range", obj.ContentRange)
w.WriteHeader(http.StatusPartialContent)
}
if _, err := io.Copy(w, obj.Body); err != nil {
h.logger.ErrorCtx(
r.Context(),
"cannot stream public file",
log.Error(err),
log.String("file_id", fileIDStr),
)
return
}
}