memos/internal/motionphoto/reader.go
boojack b3e67399b8 feat(attachment): upload files in resumable chunks
Add UploadAttachment, a unary RPC that accepts a file in bounded chunks
and streams it to a temp file instead of holding the whole blob in one
request body and in memory. A call carrying a spec starts an upload and
returns an opaque upload_id; later calls carry that id with
write_offset,
data, and finish_write, following the Cloud Storage WriteObject shape.
Retrying the last chunk after a lost response is idempotent, uploads are
bound to their owner, and they expire after 30 minutes of inactivity.

CreateAttachment and the chunked finalize now share one processing
pipeline for motion-photo detection, EXIF stripping, and storage. The
motion-photo detector works over an io.ReaderAt so large JPEGs are never
loaded whole, and the request body cap is a single per-procedure lookup.

The web editor uploads through the new RPC in 2 MiB chunks with bounded
concurrency under the server's per-user limit.
2026-09-08 21:07:06 +08:00

56 lines
1.7 KiB
Go

package motionphoto
import (
"bytes"
"io"
"strconv"
)
// DetectJPEGReader detects embedded video without loading the JPEG into memory.
// It reads at most the metadata header plus the blocks needed to locate the video.
func DetectJPEGReader(reader io.ReaderAt, size int64) (*Detection, error) {
if size < 16 {
return nil, nil
}
header := make([]byte, min(size, maxMetadataScanBytes))
if _, err := reader.ReadAt(header, 0); err != nil {
return nil, err
}
if !bytes.HasPrefix(header, []byte{0xFF, 0xD8}) || !motionPhotoMarkerRegex.Match(header) {
return nil, nil
}
text := string(header)
makeDetection := func(start int64) *Detection {
return &Detection{VideoStart: int(start), PresentationTimestampUs: parsePresentationTimestampUs(text)}
}
if matches := microVideoOffsetRegex.FindStringSubmatch(text); len(matches) == 2 {
if offset, err := strconv.ParseInt(matches[1], 10, 64); err == nil && offset >= 12 && offset < size {
var probe [12]byte
if _, err := reader.ReadAt(probe[:], size-offset); err != nil {
return nil, err
}
if looksLikeMP4(probe[:]) {
return makeDetection(size - offset), nil
}
}
}
// Fall back to the last valid ftyp box, scanning backwards with overlap so
// a box header crossing a block boundary is still found.
buffer := make([]byte, 64*1024)
for end := size; end >= 12; {
start := max(int64(0), end-int64(len(buffer)))
block := buffer[:end-start]
if _, err := reader.ReadAt(block, start); err != nil {
return nil, err
}
if index := findEmbeddedMP4Start(block); index >= 0 {
return makeDetection(start + int64(index)), nil
}
if start == 0 {
break
}
end = start + 11
}
return nil, nil
}