package archiver import ( "archive/tar" "bytes" "compress/gzip" "embed" "errors" "github.com/go-git/go-billy/v5" "io/fs" "os" "regexp" "sync" ) type Type string var ( ErrArchiveTypeNotSupported = errors.New("archive type not supported") ErrArchiveFileNotFound = errors.New("archive file not found") ) const ( TarGzip Type = "tar.gz" TarBzip = "tar.bz2" TarXz = "tar.xz" Tar = "tar" Zip = "zip" ) type Archive struct { // Type of Archive Type Type // Path of Archive file Path string reader interface{} tarReader *tar.Reader // Used for anything with .tar due to how tar.Reader cannot be reset. files map[string]*File archiveFile *bytes.Reader mu sync.Mutex } // Filesystem represents a standard interface for filesystems. // Currently, supports fs.FS and billy.Filesystem. type Filesystem struct { billyFS billy.Filesystem fs fs.FS embed *embed.FS file bool path string } // WrapBillyFS wraps billy.Filesystem to the Filesystem interface. // Returns Filesystem. func WrapBillyFS(filesystem billy.Filesystem) Filesystem { return Filesystem{ billyFS: filesystem, } } // WrapFS wraps fs.FS to the Filesystem interface. // Returns Filesystem. func WrapFS(filesystem fs.FS) Filesystem { return Filesystem{ fs: filesystem, } } // WrapPath wraps a path to the Filesystem interface. // Returns Filesystem. func WrapPath(path string) Filesystem { return Filesystem{ path: path, } } // ExtractOptions represents options for extracting an Archive type ExtractOptions struct { // Whether to overwrite files if they already exist. Overwrite bool // Folder to extract Archive to. Folder string // Whether to preserve the file structure in the Archive or not. NotPreserveFileStructure bool // Optional regex filter for specific files. Filter *regexp.Regexp } // ExtractFileOptions represents options for extracting an ArchiveFile type ExtractFileOptions struct { // Whether to overwrite files if they already exist. Overwrite bool // Perms for output file, default is the permission in the Archive. Perms os.FileMode // Folder to extract Archive to. Folder string // Whether to preserve the file structure in the Archive or not. NotPreserveFileStructure bool } // File represents a file in an archive. type File struct { // Name of file (e.g "test.txt") FileName string // Path of file in archive (e.g "folder/test.txt") Path string archive *Archive } // GetFile gets a File from the Archive // It takes the path of the file in the Archive as its parameter. // The function returns ArchiveFile and an error, if any. func (a *Archive) GetFile(path string) (*File, error) { a.mu.Lock() defer a.mu.Unlock() file, ok := a.files[path] if !ok { return nil, ErrArchiveFileNotFound } return file, nil } // FileCount gets the total file count in the Archive. // The function returns an integer. func (a *Archive) FileCount() int { return len(a.files) } // ExtractBillyFS extracts the specified Archive into billy.Filesystem. // It takes billy.Filesystem and ExtractOptions as its parameters. // The function returns an error, if any. func (a *Archive) ExtractBillyFS(filesystem billy.Filesystem, opts ExtractOptions) error { return extract(WrapBillyFS(filesystem), opts, a) } // Extract extracts the specified Archive to a specified directory. // It takes ExtractOptions as its parameter // The function returns an error, if any. func (a *Archive) Extract(opts ExtractOptions) error { return extract(Filesystem{file: true}, opts, a) } // ExtractFile extracts a specified file from an archive to a specified directory. // It takes a file path, and ExtractFileOptions as its parameters. // The function returns an error, if any. func (a *Archive) ExtractFile(path string, opts ExtractFileOptions) error { file, err := a.GetFile(path) if err != nil { return err } return file.Extract(opts) } // Close closes the Archive. // The function returns an error, if any. func (a *Archive) Close() error { a.archiveFile = nil switch a.Type { case TarGzip: if err := a.reader.(*gzip.Reader).Close(); err != nil { return err } } return nil } // ExtractBillyFS extracts the specified File in the archive into billy.Filesystem. // It takes billy.Filesystem and ExtractFileOptions as its parameters. // The function returns an error, if any. func (f *File) ExtractBillyFS(filesystem billy.Filesystem, opts ExtractFileOptions) error { return extractFile(WrapBillyFS(filesystem), opts, f) } // Extract extracts the specified File in the archive to a specified directory. // It takes ExtractFileOptions as its parameter. // The function returns an error, if any. func (f *File) Extract(opts ExtractFileOptions) error { return extractFile(Filesystem{file: true}, opts, f) } // Read reads the specified File in the archive and returns the content. // The function returns the content of the File, and an error, if any. func (f *File) Read() ([]byte, error) { switch f.archive.Type { case Tar, TarGzip, TarBzip, TarXz: return tarRead(f) case Zip: return zipRead(f) default: return nil, ErrArchiveTypeNotSupported } }