Stream post videos to Spaces.
CI / test (pull_request) Successful in 6m31s

Pass the spooled video file through instead of buffering it in RAM, and sign Spaces uploads as UNSIGNED-PAYLOAD so the client does not hash the body first.
This commit is contained in:
2026-08-30 23:31:32 -07:00
parent a6e414853d
commit 1b002a6409
4 changed files with 79 additions and 27 deletions
+22 -16
View File
@@ -190,13 +190,14 @@ func (s *Server) uploadPostMedia(
if err != nil {
return store.PostImage{}, "", err
}
defer prepared.body.Close()
mediaID := uuid.NewString()
objectKey := path.Join("post-videos", postID, mediaID+prepared.extension)
publicURL, err := s.cfg.Blob.Upload(ctx, blob.FileUpload{
Key: objectKey,
Body: bytes.NewReader(prepared.body),
Body: prepared.body,
ContentType: prepared.contentType,
Size: int64(len(prepared.body)),
Size: prepared.size,
})
if err != nil {
return store.PostImage{}, "", &postImageRequestError{
@@ -405,7 +406,8 @@ func isWebM(raw []byte) bool {
}
type preparedPostVideo struct {
body []byte
body io.ReadCloser
size int64
extension string
contentType string
}
@@ -414,6 +416,9 @@ func preparePostVideo(header *multipart.FileHeader) (preparedPostVideo, error) {
if header == nil {
return preparedPostVideo{}, invalidPostImage("Select a valid video.", nil)
}
if header.Size == 0 {
return preparedPostVideo{}, invalidPostImage("Videos cannot be empty.", nil)
}
if header.Size > postVideoMaxFileBytes {
return preparedPostVideo{}, &postImageRequestError{
status: http.StatusRequestEntityTooLarge,
@@ -424,27 +429,28 @@ func preparePostVideo(header *multipart.FileHeader) (preparedPostVideo, error) {
if err != nil {
return preparedPostVideo{}, invalidPostImage("Could not read video.", err)
}
defer file.Close()
raw, err := io.ReadAll(io.LimitReader(file, postVideoMaxFileBytes+1))
if err != nil {
peek := make([]byte, 512)
n, err := io.ReadFull(file, peek)
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
file.Close()
return preparedPostVideo{}, invalidPostImage("Could not read video.", err)
}
if len(raw) == 0 {
if n == 0 {
file.Close()
return preparedPostVideo{}, invalidPostImage("Videos cannot be empty.", nil)
}
if int64(len(raw)) > postVideoMaxFileBytes {
return preparedPostVideo{}, &postImageRequestError{
status: http.StatusRequestEntityTooLarge,
message: "Each video must be 25 MB or smaller.",
}
}
switch mediaKindFromBytes(raw) {
switch mediaKindFromBytes(peek[:n]) {
case store.MediaKindVideo:
default:
file.Close()
return preparedPostVideo{}, invalidPostImage("Videos must be MP4 or WebM.", nil)
}
result := preparedPostVideo{body: raw}
if isWebM(raw) {
if _, err := file.Seek(0, io.SeekStart); err != nil {
file.Close()
return preparedPostVideo{}, invalidPostImage("Could not read video.", err)
}
result := preparedPostVideo{body: file, size: header.Size}
if isWebM(peek[:n]) {
result.extension = ".webm"
result.contentType = "video/webm"
return result, nil