Files
SpotiFLAC-Mobile/go_backend/m4a_stream.go
T

121 lines
3.2 KiB
Go

package gobackend
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
)
// Streaming support for MP4 tag rewrites: only the boxes being edited (moov,
// ftyp) are held in memory while the mdat bulk is streamed between file
// handles, so peak memory tracks the moov size instead of the file size.
// loadTopLevelMP4Box scans f's top-level boxes and loads only the first box of
// the given type into memory, returning its bytes and absolute file offset.
// ok is false when the box does not exist; err reports read/parse failures.
func loadTopLevelMP4Box(f *os.File, fileSize int64, typ string) (buf []byte, offset int64, ok bool, err error) {
for pos := int64(0); pos+8 <= fileSize; {
header, err := readAtomHeaderAt(f, pos, fileSize)
if err != nil {
return nil, 0, false, err
}
size := header.size
if size == 0 {
size = fileSize - pos
}
if size < header.headerSize || pos+size > fileSize {
return nil, 0, false, fmt.Errorf("invalid atom size for %s", header.typ)
}
if header.typ == typ {
buf := make([]byte, size)
if _, err := f.ReadAt(buf, pos); err != nil {
return nil, 0, false, err
}
return buf, pos, true, nil
}
pos += size
}
return nil, 0, false, nil
}
// fileSection is one byte range [start,end) to substitute during a streaming
// rewrite.
type fileSection struct {
start, end int64
data []byte
}
// replaceFileSectionsStreaming rewrites filePath with each section replaced by
// its data, streaming every byte outside the sections, and publishes via
// temp+fsync+rename so an interruption never leaves a truncated file under the
// final name. Sections must not overlap; they are sorted internally.
func replaceFileSectionsStreaming(filePath string, sections []fileSection) error {
sorted := append([]fileSection{}, sections...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].start < sorted[j].start })
for i := 1; i < len(sorted); i++ {
if sorted[i].start < sorted[i-1].end {
return fmt.Errorf("overlapping file sections")
}
}
src, err := os.Open(filePath)
if err != nil {
return err
}
tmpPath := filePath + ".tag.partial"
os.Remove(tmpPath)
tmp, err := os.Create(tmpPath)
if err != nil {
src.Close()
return err
}
fail := func(err error) error {
tmp.Close()
src.Close()
os.Remove(tmpPath)
return err
}
pos := int64(0)
for _, sec := range sorted {
if sec.start < pos {
return fail(fmt.Errorf("file section out of order"))
}
if _, err := io.CopyN(tmp, src, sec.start-pos); err != nil {
return fail(err)
}
if _, err := tmp.Write(sec.data); err != nil {
return fail(err)
}
if _, err := src.Seek(sec.end, io.SeekStart); err != nil {
return fail(err)
}
pos = sec.end
}
if _, err := io.Copy(tmp, src); err != nil {
return fail(err)
}
if err := tmp.Sync(); err != nil {
return fail(err)
}
if err := tmp.Close(); err != nil {
src.Close()
os.Remove(tmpPath)
return err
}
// Release the read handle before the rename (required on Windows).
if err := src.Close(); err != nil {
os.Remove(tmpPath)
return err
}
if err := os.Rename(tmpPath, filePath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to publish tagged file: %w", err)
}
syncDir(filepath.Dir(filePath))
return nil
}