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.
56 lines
1.7 KiB
Go
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
|
|
}
|