mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
121 lines
3.2 KiB
Go
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
|
|
}
|