diff --git a/internal/backends/spotifyhistory/archive.go b/internal/backends/spotifyhistory/archive.go index cb53772..2f9a2ec 100644 --- a/internal/backends/spotifyhistory/archive.go +++ b/internal/backends/spotifyhistory/archive.go @@ -21,7 +21,7 @@ import ( "errors" "sort" - "go.uploadedlobster.com/scotty/internal/archive" + "go.uploadedlobster.com/scotty/pkg/archive" ) var historyFileGlobs = []string{ diff --git a/internal/listenbrainz/archive.go b/internal/listenbrainz/archive.go index 57c3ad8..68740aa 100644 --- a/internal/listenbrainz/archive.go +++ b/internal/listenbrainz/archive.go @@ -32,7 +32,7 @@ import ( "time" "github.com/simonfrey/jsonl" - "go.uploadedlobster.com/scotty/internal/archive" + "go.uploadedlobster.com/scotty/pkg/archive" ) // Represents a ListenBrainz export archive. diff --git a/internal/archive/archive.go b/pkg/archive/archive.go similarity index 61% rename from internal/archive/archive.go rename to pkg/archive/archive.go index 3c2ee86..41c954f 100644 --- a/internal/archive/archive.go +++ b/pkg/archive/archive.go @@ -27,12 +27,10 @@ THE SOFTWARE. package archive import ( - "archive/zip" "fmt" "io" "io/fs" "os" - "path/filepath" ) // Generic interface to access files inside an archive. @@ -101,102 +99,3 @@ type filesystemFile struct { func (f *filesystemFile) Open() (io.ReadCloser, error) { return os.Open(f.path) } - -// An implementation of the archiveBackend interface for zip files. -type zipArchive struct { - zip *zip.ReadCloser -} - -func (a *zipArchive) OpenArchive(path string) error { - zip, err := zip.OpenReader(path) - if err != nil { - return err - } - a.zip = zip - return nil -} - -func (a *zipArchive) Close() error { - if a.zip == nil { - return nil - } - return a.zip.Close() -} - -func (a *zipArchive) Glob(pattern string) ([]FileInfo, error) { - result := make([]FileInfo, 0) - for _, file := range a.zip.File { - if file.FileInfo().IsDir() { - continue - } - - if matched, err := filepath.Match(pattern, file.Name); matched { - if err != nil { - return nil, err - } - info := FileInfo{ - Name: file.Name, - File: file, - } - result = append(result, info) - } - } - - return result, nil -} - -func (a *zipArchive) Open(path string) (fs.File, error) { - file, err := a.zip.Open(path) - if err != nil { - return nil, err - } - return file, nil -} - -// An implementation of the archiveBackend interface for directories. -type dirArchive struct { - path string - dirFS fs.FS -} - -func (a *dirArchive) OpenArchive(path string) error { - a.path = filepath.Clean(path) - a.dirFS = os.DirFS(path) - return nil -} - -func (a *dirArchive) Close() error { - return nil -} - -// Open opens the named file in the archive. -// [fs.File.Close] must be called to release any associated resources. -func (a *dirArchive) Open(path string) (fs.File, error) { - return a.dirFS.Open(path) -} - -func (a *dirArchive) Glob(pattern string) ([]FileInfo, error) { - files, err := fs.Glob(a.dirFS, pattern) - if err != nil { - return nil, err - } - result := make([]FileInfo, 0) - for _, name := range files { - stat, err := fs.Stat(a.dirFS, name) - if err != nil { - return nil, err - } - if stat.IsDir() { - continue - } - - fullPath := filepath.Join(a.path, name) - info := FileInfo{ - Name: name, - File: &filesystemFile{path: fullPath}, - } - result = append(result, info) - } - - return result, nil -} diff --git a/internal/archive/archive_test.go b/pkg/archive/archive_test.go similarity index 98% rename from internal/archive/archive_test.go rename to pkg/archive/archive_test.go index 6f3f6db..f1bbd07 100644 --- a/internal/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -29,7 +29,7 @@ import ( "slices" "testing" - "go.uploadedlobster.com/scotty/internal/archive" + "go.uploadedlobster.com/scotty/pkg/archive" ) func ExampleOpenArchive() { diff --git a/pkg/archive/dir.go b/pkg/archive/dir.go new file mode 100644 index 0000000..166e70b --- /dev/null +++ b/pkg/archive/dir.go @@ -0,0 +1,77 @@ +/* +Copyright © 2025 Philipp Wolfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package archive + +import ( + "io/fs" + "os" + "path/filepath" +) + +// An implementation of the [ArchiveReader] interface for directories. +type dirArchive struct { + path string + dirFS fs.FS +} + +func (a *dirArchive) OpenArchive(path string) error { + a.path = filepath.Clean(path) + a.dirFS = os.DirFS(path) + return nil +} + +func (a *dirArchive) Close() error { + return nil +} + +// Open opens the named file in the archive. +// [fs.File.Close] must be called to release any associated resources. +func (a *dirArchive) Open(path string) (fs.File, error) { + return a.dirFS.Open(path) +} + +func (a *dirArchive) Glob(pattern string) ([]FileInfo, error) { + files, err := fs.Glob(a.dirFS, pattern) + if err != nil { + return nil, err + } + result := make([]FileInfo, 0) + for _, name := range files { + stat, err := fs.Stat(a.dirFS, name) + if err != nil { + return nil, err + } + if stat.IsDir() { + continue + } + + fullPath := filepath.Join(a.path, name) + info := FileInfo{ + Name: name, + File: &filesystemFile{path: fullPath}, + } + result = append(result, info) + } + + return result, nil +} diff --git a/internal/archive/testdata/archive.zip b/pkg/archive/testdata/archive.zip similarity index 100% rename from internal/archive/testdata/archive.zip rename to pkg/archive/testdata/archive.zip diff --git a/internal/archive/testdata/archive/a/1.txt b/pkg/archive/testdata/archive/a/1.txt similarity index 100% rename from internal/archive/testdata/archive/a/1.txt rename to pkg/archive/testdata/archive/a/1.txt diff --git a/internal/archive/testdata/archive/b/1.txt b/pkg/archive/testdata/archive/b/1.txt similarity index 100% rename from internal/archive/testdata/archive/b/1.txt rename to pkg/archive/testdata/archive/b/1.txt diff --git a/internal/archive/testdata/archive/b/2.txt b/pkg/archive/testdata/archive/b/2.txt similarity index 100% rename from internal/archive/testdata/archive/b/2.txt rename to pkg/archive/testdata/archive/b/2.txt diff --git a/pkg/archive/zip.go b/pkg/archive/zip.go new file mode 100644 index 0000000..0054cf3 --- /dev/null +++ b/pkg/archive/zip.go @@ -0,0 +1,80 @@ +/* +Copyright © 2025 Philipp Wolfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package archive + +import ( + "archive/zip" + "io/fs" + "path/filepath" +) + +// An implementation of the [ArchiveReader] interface for zip files. +type zipArchive struct { + zip *zip.ReadCloser +} + +func (a *zipArchive) OpenArchive(path string) error { + zip, err := zip.OpenReader(path) + if err != nil { + return err + } + a.zip = zip + return nil +} + +func (a *zipArchive) Close() error { + if a.zip == nil { + return nil + } + return a.zip.Close() +} + +func (a *zipArchive) Glob(pattern string) ([]FileInfo, error) { + result := make([]FileInfo, 0) + for _, file := range a.zip.File { + if file.FileInfo().IsDir() { + continue + } + + if matched, err := filepath.Match(pattern, file.Name); matched { + if err != nil { + return nil, err + } + info := FileInfo{ + Name: file.Name, + File: file, + } + result = append(result, info) + } + } + + return result, nil +} + +func (a *zipArchive) Open(path string) (fs.File, error) { + file, err := a.zip.Open(path) + if err != nil { + return nil, err + } + return file, nil +}