package asset import ( "bytes" "compress/gzip" "errors" "fmt" "io" "io/fs" "strings" "github.com/omeid/go-tarfs" ) type archiveLoader struct { loader Loader } // NewArchiveLoader wraps an existing Loader in order to support extracting // files from within an archive file asset. // // For example, loading the path `foo.tgz/foo/bar.jpg` will call // `Load("foo.tgz")`, load that archive into memory, and serve the file // `./foo/bar.jpg` from within the archive. func NewArchiveLoader(loader Loader) Loader { return &archiveLoader{loader: loader} } func (l *archiveLoader) Load(path string, into io.Writer, opts LoadOpts) error { id, subPath, ok := strings.Cut(strings.TrimPrefix(path, "/"), "/") if !ok { return l.loader.Load(path, into, opts) } var isGzipped bool switch { case strings.HasSuffix(id, ".tar.gz"), strings.HasSuffix(id, ".tgz"): isGzipped = true case strings.HasSuffix(id, ".tar"): // ok default: // unsupported return l.loader.Load(path, into, opts) } buf := new(bytes.Buffer) if err := l.loader.Load(id, buf, opts); err != nil { return fmt.Errorf("loading archive into buffer: %w", err) } var ( from io.Reader = buf err error ) if isGzipped { if from, err = gzip.NewReader(from); err != nil { return fmt.Errorf("decompressing archive asset with id %q: %w", id, err) } } tarFS, err := tarfs.New(from) if err != nil { return fmt.Errorf("reading archive asset with id %q as fs: %w", id, err) } f, err := tarFS.Open(subPath) if errors.Is(err, fs.ErrExist) { return ErrNotFound } else if err != nil { return fmt.Errorf( "opening path %q from archive asset with id %q as fs: %w", subPath, id, err, ) } defer f.Close() if _, err = io.Copy(into, f); err != nil { return fmt.Errorf( "reading %q from archive asset with id %q as fs: %w", subPath, id, err, ) } return nil }