diff --git a/README.md b/README.md index 90937eb..3f0024f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Introducing **Archiver 4.0** - a cross-platform, multi-format archive utility an - Create and extract archive files - Walk or traverse into archive files - Extract only specific files from archives -- Insert (append) into .tar files +- Insert (append) into .tar and .zip archives - Read from password-protected 7-Zip files - Numerous archive and compression formats supported - Extensible (add more formats just by registering them) @@ -301,9 +301,9 @@ defer decompressor.Close() // reads from decompressor will be decompressed ``` -### Append to tarball +### Append to tarball and zip archives -Tar archives can be appended to without creating a whole new archive by calling `Insert()` on a tar stream. However, this requires that the tarball is not compressed (due to complexities with modifying compression dictionaries). +Tar and Zip archives can be appended to without creating a whole new archive by calling `Insert()` on a tar or zip stream. However, for tarballs, this requires that the tarball is not compressed (due to complexities with modifying compression dictionaries). Here is an example that appends a file to a tarball on disk: @@ -325,3 +325,5 @@ if err != nil { } ``` +The code is similar for inserting into a Zip archive, except you'll call `Insert()` on the `Zip` type instead. + diff --git a/archiver.go b/archiver.go index 1701b07..c968e1f 100644 --- a/archiver.go +++ b/archiver.go @@ -27,6 +27,11 @@ type File struct { // it is such a common field and we want to preserve // format-agnosticism (no type assertions) for basic // operations. + // + // EXPERIMENTAL: If inserting a file into an archive, + // and this is left blank, the implementation of the + // archive format can default to using the file's base + // name. NameInArchive string // For symbolic and hard links, the target of the link. @@ -224,7 +229,7 @@ func openAndCopyFile(file File, w io.Writer) error { // When file is in use and size is being written to, creating the compressed // file will fail with "archive/tar: write too long." Using CopyN gracefully // handles this. - _, err = io.CopyN(w, fileReader, file.Size()) + _, err = io.Copy(w, fileReader) if err != nil && err != io.EOF { return err } diff --git a/go.mod b/go.mod index 96490a8..d1e0d85 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/mholt/archiver/v4 -go 1.20 +go 1.21.3 + +toolchain go1.22.0 require ( github.com/andybalholm/brotli v1.0.5 @@ -13,6 +15,7 @@ require ( ) require ( + github.com/STARRY-S/zip v0.1.0 github.com/bodgit/sevenzip v1.4.3 github.com/golang/snappy v0.0.4 github.com/pierrec/lz4/v4 v4.1.18 diff --git a/go.sum b/go.sum index 3df54f5..c352a82 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= +github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= @@ -104,6 +106,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= diff --git a/interfaces.go b/interfaces.go index bfc5316..9a41e1f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -94,6 +94,7 @@ type Extractor interface { } // Inserter can insert files into an existing archive. +// EXPERIMENTAL: This API is subject to change. type Inserter interface { // Insert inserts the files into archive. // diff --git a/tar.go b/tar.go index c07efb4..1611691 100644 --- a/tar.go +++ b/tar.go @@ -84,6 +84,9 @@ func (t Tar) writeFileToArchive(ctx context.Context, tw *tar.Writer, file File) return fmt.Errorf("file %s: creating header: %w", file.NameInArchive, err) } hdr.Name = file.NameInArchive // complete path, since FileInfoHeader() only has base name + if hdr.Name == "" { + hdr.Name = file.Name() // assume base name of file I guess + } if t.NumericUIDGID { hdr.Uname = "" hdr.Gname = "" diff --git a/zip.go b/zip.go index 421fe6e..0694d7e 100644 --- a/zip.go +++ b/zip.go @@ -11,6 +11,8 @@ import ( "path" "strings" + szip "github.com/STARRY-S/zip" + "github.com/dsnet/compress/bzip2" "github.com/klauspost/compress/zip" "github.com/klauspost/compress/zstd" @@ -137,6 +139,9 @@ func (z Zip) archiveOneFile(ctx context.Context, zw *zip.Writer, idx int, file F return fmt.Errorf("getting info for file %d: %s: %w", idx, file.Name(), err) } hdr.Name = file.NameInArchive // complete path, since FileInfoHeader() only has base name + if hdr.Name == "" { + hdr.Name = file.Name() // assume base name of file I guess + } // customize header based on file properties if file.IsDir() { @@ -256,6 +261,66 @@ func (z Zip) decodeText(hdr *zip.FileHeader) { } } +// Insert appends the listed files into the provided Zip archive stream. +func (z Zip) Insert(ctx context.Context, into io.ReadWriteSeeker, files []File) error { + // following very simple example at https://github.com/STARRY-S/zip?tab=readme-ov-file#usage + zu, err := szip.NewUpdater(into) + if err != nil { + return err + } + defer zu.Close() + + for idx, file := range files { + if err := ctx.Err(); err != nil { + return err // honor context cancellation + } + + hdr, err := szip.FileInfoHeader(file) + if err != nil { + return fmt.Errorf("getting info for file %d: %s: %w", idx, file.NameInArchive, err) + } + hdr.Name = file.NameInArchive // complete path, since FileInfoHeader() only has base name + if hdr.Name == "" { + hdr.Name = file.Name() // assume base name of file I guess + } + + // customize header based on file properties + if file.IsDir() { + if !strings.HasSuffix(hdr.Name, "/") { + hdr.Name += "/" // required + } + hdr.Method = zip.Store + } else if z.SelectiveCompression { + // only enable compression on compressable files + ext := strings.ToLower(path.Ext(hdr.Name)) + if _, ok := compressedFormats[ext]; ok { + hdr.Method = zip.Store + } else { + hdr.Method = z.Compression + } + } + + w, err := zu.AppendHeaderAt(hdr, -1) + if err != nil { + return fmt.Errorf("inserting file header: %d: %s: %w", idx, file.Name(), err) + } + + // directories have no file body + if file.IsDir() { + return nil + } + if err := openAndCopyFile(file, w); err != nil { + if z.ContinueOnError && ctx.Err() == nil { + log.Printf("[ERROR] appending file %d into archive: %s: %v", idx, file.Name(), err) + continue + } + return fmt.Errorf("copying inserted file %d: %s: %w", idx, file.Name(), err) + } + } + + return nil +} + type seekReaderAt interface { io.ReaderAt io.Seeker