package uploads import ( "context" "net/http" "os" "strconv" "time" "github.com/google/uuid" "pipu/api/internal/api/gen" "pipu/api/internal/platform/apperr" "pipu/api/internal/platform/httpx" ) // uploadStream serves private staging files through the API with Range support. // Nginx runs as another user and cannot read tus files (0750 directories, 0640 files). type uploadStream struct { req *http.Request file string format string } func (s uploadStream) VisitStreamUploadResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", contentType(s.format)) w.Header().Set("Cache-Control", "private, max-age=600") f, err := os.Open(s.file) if err != nil { return err } defer func() { _ = f.Close() }() info, err := f.Stat() if err != nil { return err } if s.req == nil { http.ServeContent(w, &http.Request{}, "", info.ModTime(), f) return nil } http.ServeContent(w, s.req, "", info.ModTime(), f) return nil } func (h *Handler) streamURL(id uuid.UUID) string { exp := time.Now().Add(12 * time.Hour).Unix() sig := h.signer.Sign("upload", id.String(), strconv.FormatInt(exp, 10)) return "/api/v1/admin/uploads/files/" + id.String() + "/stream?exp=" + strconv.FormatInt(exp, 10) + "&sig=" + sig } // StreamUpload реализует gen.StrictServerInterface. func (h *Handler) StreamUpload(ctx context.Context, req gen.StreamUploadRequestObject) (gen.StreamUploadResponseObject, error) { if h.signer == nil || !h.signer.Verify(req.Params.Sig, "upload", req.Id.String(), strconv.FormatInt(req.Params.Exp, 10)) || time.Now().Unix() > req.Params.Exp { return nil, apperr.Forbidden("uploads.link_invalid", "link is invalid or expired") } f, err := h.svc.OpenStaging(ctx, req.Id) if err != nil { return nil, err } format := "" if f.Format != nil { format = *f.Format } return uploadStream{ req: httpx.RequestFrom(ctx), file: f.Abs, format: format, }, nil } func contentType(format string) string { switch format { case "mp3": return "audio/mpeg" case "flac": return "audio/flac" case "m4a": return "audio/mp4" case "ogg", "opus": return "audio/ogg" case "wav": return "audio/wav" default: return "application/octet-stream" } }