ci / api / api (push) Successful in 40s
ci / db / db (push) Successful in 1m3s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Failing after 1m48s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 46s
ci / ts / ts (packages + web) (push) Failing after 4m57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 5m7s
Quality upgrades, batch covers, staging preview, and resume after reload are in the queue. Loudness and crossfade work on web and Android, and the documented checks now have suites. Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
2.3 KiB
Go
93 lines
2.3 KiB
Go
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/platform/apperr"
|
|
"pipu/api/internal/platform/httpx"
|
|
)
|
|
|
|
// uploadStream — отдача файла staging с Range (сам api или X-Accel-Redirect).
|
|
type uploadStream struct {
|
|
req *http.Request
|
|
file string
|
|
format string
|
|
accel string
|
|
xAccel bool
|
|
}
|
|
|
|
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")
|
|
if s.xAccel && s.accel != "" {
|
|
w.Header().Set("X-Accel-Redirect", s.accel)
|
|
w.WriteHeader(http.StatusOK)
|
|
return nil
|
|
}
|
|
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, xAccel: h.xAccel,
|
|
accel: "/_internal/uploads/" + filepath.ToSlash(f.StagingPath),
|
|
}, 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"
|
|
}
|
|
}
|