Skip to content

Commit

Permalink
zip: Implement Insert (append files)
Browse files Browse the repository at this point in the history
Close #397

Also minor tweaks
  • Loading branch information
mholt committed Feb 13, 2024
1 parent 81f9e06 commit 43a073e
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 5 deletions.
8 changes: 5 additions & 3 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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:

Expand All @@ -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.

7 changes: 6 additions & 1 deletion archiver.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion 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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions interfaces.go
Expand Up @@ -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.
//
Expand Down
3 changes: 3 additions & 0 deletions tar.go
Expand Up @@ -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 = ""
Expand Down
65 changes: 65 additions & 0 deletions zip.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 43a073e

Please sign in to comment.