Files
SpotiFLAC-Mobile/go_backend/flac_save.go
T

70 lines
2.1 KiB
Go

package gobackend
import (
"fmt"
"os"
"path/filepath"
"strings"
flac "github.com/go-flac/go-flac/v2"
)
// saveFlacFile persists an edited FLAC crash-safely. go-flac's Save(samePath)
// rewrites the file in place by shifting the audio body within the same inode,
// so a process kill or power loss mid-save destroys the file with no recovery
// copy. Instead, stream the whole edited file to a sibling temp, fsync it, and
// rename it over the target: an interruption at any point leaves either the
// old intact file or the new complete one.
//
// fd-backed targets (/proc/self/fd/N) have no directory entry to rename over,
// so those keep the library's in-place path.
func saveFlacFile(f *flac.File, filePath string) error {
if strings.HasPrefix(filePath, "/proc/self/fd/") {
return f.Save(filePath)
}
// The ".partial" suffix keeps the temp invisible to library scans and
// extension duplicate checks; the extra ".tag" avoids colliding with the
// download staging sibling of the same final path.
tmpPath := filePath + ".tag.partial"
os.Remove(tmpPath)
tmp, err := os.Create(tmpPath)
if err != nil {
return fmt.Errorf("failed to create temp tag file: %w", err)
}
// WriteTo streams the audio frames from the still-open source handle and
// closes it when done, so the rename below can replace the source even on
// Windows.
if _, err := f.WriteTo(tmp); err != nil {
tmp.Close()
os.Remove(tmpPath)
return err
}
if err := tmp.Sync(); err != nil {
tmp.Close()
os.Remove(tmpPath)
return fmt.Errorf("failed to sync temp tag file: %w", err)
}
if err := tmp.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
}
// syncDir best-effort fsyncs a directory so a just-renamed entry survives
// power loss. Unsupported on some platforms/filesystems; errors are ignored.
func syncDir(dir string) {
d, err := os.Open(dir)
if err != nil {
return
}
_ = d.Sync()
_ = d.Close()
}