From a1764b1aa086c9566627f2ff39cc04064068e604 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 9 Jul 2026 19:37:43 +0700 Subject: [PATCH] fix(metadata): close FLAC file handles after parsing flac.ParseFile leaks the *os.File on parse errors, and read-only callers (ReadMetadata, ExtractCoverArt, extractLyricsFromFlac) never closed the handle on success either. Long library or duplicate-index scans leaked one fd per FLAC file, and on Windows the lingering handles kept files locked (the source of the flaky TempDir cleanup failure in TestExportsJSONWrappersAndExtensionManagerSurface). Route all parsing through parseFlacFile, which closes on error, and defer f.Close() at every call site. File.Save already closes the underlying handle; the deferred second Close is a harmless no-op. --- go_backend/metadata.go | 45 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 6576e2f0..42e6345f 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -200,11 +200,30 @@ type Metadata struct { ReplayGainAlbumPeak string // e.g. "1.000000" } +// parseFlacFile wraps flac.ParseFile but closes the file handle when parsing +// fails. flac.ParseFile leaks the *os.File on parse errors (no reference is +// returned to close it), which on Windows keeps the file locked until GC. +// Callers must Close() the returned file when done reading; File.Save also +// closes the underlying handle, and a second Close afterwards is harmless. +func parseFlacFile(filePath string) (*flac.File, error) { + handle, err := os.Open(filePath) + if err != nil { + return nil, err + } + f, err := flac.ParseBytes(flac.NewBufIOWithInner(handle)) + if err != nil { + handle.Close() + return nil, err + } + return f, nil +} + func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -262,10 +281,11 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { } func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []byte) error { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -314,10 +334,11 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] } func ReadMetadata(filePath string) (*Metadata, error) { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return nil, fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() metadata := &Metadata{} @@ -403,10 +424,11 @@ func ReadMetadata(filePath string) (*Metadata, error) { // absent from the map are left untouched. This is the correct function for // partial edits (e.g. writing only ReplayGain tags) and full editor saves alike. func EditFlacFields(filePath string, fields map[string]string) error { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -686,10 +708,11 @@ func RewriteSplitArtistTags(filePath, artist, albumArtist string) error { return nil } - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -821,10 +844,11 @@ func fileExists(path string) bool { } func ExtractCoverArt(filePath string) ([]byte, error) { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return nil, fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() for _, meta := range f.Meta { if meta.Type == flac.Picture { @@ -854,10 +878,11 @@ func ExtractCoverArt(filePath string) ([]byte, error) { } func EmbedLyrics(filePath string, lyrics string) error { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -895,10 +920,11 @@ func EmbedGenreLabel(filePath string, genre, label string) error { return nil } - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -1701,10 +1727,11 @@ func extractLyricsFromSidecarLRC(filePath string) (string, error) { } func extractLyricsFromFlac(filePath string) (string, error) { - f, err := flac.ParseFile(filePath) + f, err := parseFlacFile(filePath) if err != nil { return "", fmt.Errorf("failed to parse FLAC file: %w", err) } + defer f.Close() for _, meta := range f.Meta { if meta.Type != flac.VorbisComment {