Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit 2efff02

Browse files
authored
Add ArchiveAsync(); context no longer optional (#320)
Following Go best-practices. Zip and Tar now support ArchiveAsync, which reads files from a channel instead of a slice.
1 parent 2cedd0c commit 2efff02

File tree

5 files changed

+104
-63
lines changed

5 files changed

+104
-63
lines changed

fs.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ type ArchiveFS struct {
221221
Context context.Context // optional
222222
}
223223

224+
// context always return a context, preferring f.Context if not nil.
225+
func (f ArchiveFS) context() context.Context {
226+
if f.Context != nil {
227+
return f.Context
228+
}
229+
return context.Background()
230+
}
231+
224232
// Open opens the named file from within the archive. If name is "." then
225233
// the archive file itself will be opened as a directory file.
226234
func (f ArchiveFS) Open(name string) (fs.File, error) {
@@ -312,7 +320,7 @@ func (f ArchiveFS) Open(name string) (fs.File, error) {
312320
inputStream = io.NewSectionReader(f.Stream, 0, f.Stream.Size())
313321
}
314322

315-
err = f.Format.Extract(f.Context, inputStream, []string{name}, handler)
323+
err = f.Format.Extract(f.context(), inputStream, []string{name}, handler)
316324
if err != nil && fsFile != nil {
317325
if ef, ok := fsFile.(extractedFile); ok {
318326
if ef.parentArchive != nil {
@@ -377,7 +385,7 @@ func (f ArchiveFS) Stat(name string) (fs.FileInfo, error) {
377385
if f.Stream != nil {
378386
inputStream = io.NewSectionReader(f.Stream, 0, f.Stream.Size())
379387
}
380-
err = f.Format.Extract(f.Context, inputStream, []string{name}, handler)
388+
err = f.Format.Extract(f.context(), inputStream, []string{name}, handler)
381389
if err != nil && result.FileInfo == nil {
382390
return nil, err
383391
}
@@ -446,7 +454,7 @@ func (f ArchiveFS) ReadDir(name string) ([]fs.DirEntry, error) {
446454
inputStream = io.NewSectionReader(f.Stream, 0, f.Stream.Size())
447455
}
448456

449-
err = f.Format.Extract(f.Context, inputStream, filter, handler)
457+
err = f.Format.Extract(f.context(), inputStream, filter, handler)
450458
return entries, err
451459
}
452460

interfaces.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,22 @@ type Decompressor interface {
5656
type Archiver interface {
5757
// Archive writes an archive file to output with the given files.
5858
//
59-
// Context is optional, but if given, cancellation must be honored.
59+
// Context cancellation must be honored.
6060
Archive(ctx context.Context, output io.Writer, files []File) error
6161
}
6262

63+
// ArchiverAsync is an Archiver that can also create archives
64+
// asynchronously by pumping files into a channel as they are
65+
// discovered.
66+
type ArchiverAsync interface {
67+
Archiver
68+
69+
// Use ArchiveAsync if you can't pre-assemble a list of all
70+
// the files for the archive. Close the files channel after
71+
// all the files have been sent.
72+
ArchiveAsync(ctx context.Context, output io.Writer, files <-chan File) error
73+
}
74+
6375
// Extractor can extract files from an archive.
6476
type Extractor interface {
6577
// Extract reads the files at pathsInArchive from sourceArchive.
@@ -68,14 +80,14 @@ type Extractor interface {
6880
// If a path refers to a directory, all files within it are extracted.
6981
// Extracted files are passed to the handleFile callback for handling.
7082
//
71-
// Context is optional, but if given, cancellation must be honored.
83+
// Context cancellation must be honored.
7284
Extract(ctx context.Context, sourceArchive io.Reader, pathsInArchive []string, handleFile FileHandler) error
7385
}
7486

7587
// Inserter can insert files into an existing archive.
7688
type Inserter interface {
7789
// Insert inserts the files into archive.
7890
//
79-
// Context is optional, but if given, cancellation must be honored.
91+
// Context cancellation must be honored.
8092
Insert(ctx context.Context, archive io.ReadWriteSeeker, files []File) error
8193
}

rar.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ func (r Rar) Archive(_ context.Context, _ io.Writer, _ []File) error {
5656
}
5757

5858
func (r Rar) Extract(ctx context.Context, sourceArchive io.Reader, pathsInArchive []string, handleFile FileHandler) error {
59-
if ctx == nil {
60-
ctx = context.Background()
61-
}
62-
6359
var options []rardecode.Option
6460
if r.Password != "" {
6561
options = append(options, rardecode.Password(r.Password))

tar.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,28 @@ func (t Tar) Match(filename string, stream io.Reader) (MatchResult, error) {
4242
}
4343

4444
func (t Tar) Archive(ctx context.Context, output io.Writer, files []File) error {
45-
if ctx == nil {
46-
ctx = context.Background()
47-
}
48-
4945
tw := tar.NewWriter(output)
5046
defer tw.Close()
5147

5248
for _, file := range files {
53-
if err := ctx.Err(); err != nil {
54-
return err // honor context cancellation
49+
if err := t.writeFileToArchive(ctx, tw, file); err != nil {
50+
if t.ContinueOnError && ctx.Err() == nil { // context errors should always abort
51+
log.Printf("[ERROR] %v", err)
52+
continue
53+
}
54+
return err
5555
}
56-
err := t.writeFileToArchive(ctx, tw, file)
57-
if err != nil {
56+
}
57+
58+
return nil
59+
}
60+
61+
func (t Tar) ArchiveAsync(ctx context.Context, output io.Writer, files <-chan File) error {
62+
tw := tar.NewWriter(output)
63+
defer tw.Close()
64+
65+
for file := range files {
66+
if err := t.writeFileToArchive(ctx, tw, file); err != nil {
5867
if t.ContinueOnError && ctx.Err() == nil { // context errors should always abort
5968
log.Printf("[ERROR] %v", err)
6069
continue
@@ -67,6 +76,10 @@ func (t Tar) Archive(ctx context.Context, output io.Writer, files []File) error
6776
}
6877

6978
func (Tar) writeFileToArchive(ctx context.Context, tw *tar.Writer, file File) error {
79+
if err := ctx.Err(); err != nil {
80+
return err // honor context cancellation
81+
}
82+
7083
hdr, err := tar.FileInfoHeader(file, file.LinkTarget)
7184
if err != nil {
7285
return fmt.Errorf("file %s: creating header: %w", file.NameInArchive, err)
@@ -91,10 +104,6 @@ func (Tar) writeFileToArchive(ctx context.Context, tw *tar.Writer, file File) er
91104
}
92105

93106
func (t Tar) Insert(ctx context.Context, into io.ReadWriteSeeker, files []File) error {
94-
if ctx == nil {
95-
ctx = context.Background()
96-
}
97-
98107
// Tar files may end with some, none, or a lot of zero-byte padding. The spec says
99108
// it should end with two 512-byte trailer records consisting solely of null/0
100109
// bytes: https://www.gnu.org/software/tar/manual/html_node/Standard.html. However,
@@ -165,10 +174,6 @@ func (t Tar) Insert(ctx context.Context, into io.ReadWriteSeeker, files []File)
165174
}
166175

167176
func (t Tar) Extract(ctx context.Context, sourceArchive io.Reader, pathsInArchive []string, handleFile FileHandler) error {
168-
if ctx == nil {
169-
ctx = context.Background()
170-
}
171-
172177
tr := tar.NewReader(sourceArchive)
173178

174179
// important to initialize to non-nil, empty value due to how fileIsIncluded works

zip.go

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"io/fs"
11+
"log"
1112
"path"
1213
"strings"
1314

@@ -101,54 +102,77 @@ func (z Zip) Match(filename string, stream io.Reader) (MatchResult, error) {
101102
}
102103

103104
func (z Zip) Archive(ctx context.Context, output io.Writer, files []File) error {
104-
if ctx == nil {
105-
ctx = context.Background()
106-
}
107-
108105
zw := zip.NewWriter(output)
109106
defer zw.Close()
110107

111108
for i, file := range files {
112-
if err := ctx.Err(); err != nil {
113-
return err // honor context cancellation
109+
if err := z.archiveOneFile(ctx, zw, i, file); err != nil {
110+
return err
114111
}
112+
}
115113

116-
hdr, err := zip.FileInfoHeader(file)
117-
if err != nil {
118-
return fmt.Errorf("getting info for file %d: %s: %w", i, file.Name(), err)
119-
}
120-
hdr.Name = file.NameInArchive // complete path, since FileInfoHeader() only has base name
114+
return nil
115+
}
121116

122-
// customize header based on file properties
123-
if file.IsDir() {
124-
if !strings.HasSuffix(hdr.Name, "/") {
125-
hdr.Name += "/" // required
126-
}
127-
hdr.Method = zip.Store
128-
} else if z.SelectiveCompression {
129-
// only enable compression on compressable files
130-
ext := strings.ToLower(path.Ext(hdr.Name))
131-
if _, ok := compressedFormats[ext]; ok {
132-
hdr.Method = zip.Store
133-
} else {
134-
hdr.Method = z.Compression
117+
func (z Zip) ArchiveAsync(ctx context.Context, output io.Writer, files <-chan File) error {
118+
zw := zip.NewWriter(output)
119+
defer zw.Close()
120+
121+
var i int
122+
for file := range files {
123+
if err := z.archiveOneFile(ctx, zw, i, file); err != nil {
124+
if z.ContinueOnError && ctx.Err() == nil { // context errors should always abort
125+
log.Printf("[ERROR] %v", err)
126+
continue
135127
}
128+
return err
136129
}
130+
i++
131+
}
137132

138-
w, err := zw.CreateHeader(hdr)
139-
if err != nil {
140-
return fmt.Errorf("creating header for file %d: %s: %w", i, file.Name(), err)
141-
}
133+
return nil
134+
}
142135

143-
// directories have no file body
144-
if file.IsDir() {
145-
continue
136+
func (z Zip) archiveOneFile(ctx context.Context, zw *zip.Writer, idx int, file File) error {
137+
if err := ctx.Err(); err != nil {
138+
return err // honor context cancellation
139+
}
140+
141+
hdr, err := zip.FileInfoHeader(file)
142+
if err != nil {
143+
return fmt.Errorf("getting info for file %d: %s: %w", idx, file.Name(), err)
144+
}
145+
hdr.Name = file.NameInArchive // complete path, since FileInfoHeader() only has base name
146+
147+
// customize header based on file properties
148+
if file.IsDir() {
149+
if !strings.HasSuffix(hdr.Name, "/") {
150+
hdr.Name += "/" // required
146151
}
147-
if err := openAndCopyFile(file, w); err != nil {
148-
return fmt.Errorf("writing file %d: %s: %w", i, file.Name(), err)
152+
hdr.Method = zip.Store
153+
} else if z.SelectiveCompression {
154+
// only enable compression on compressable files
155+
ext := strings.ToLower(path.Ext(hdr.Name))
156+
if _, ok := compressedFormats[ext]; ok {
157+
hdr.Method = zip.Store
158+
} else {
159+
hdr.Method = z.Compression
149160
}
150161
}
151162

163+
w, err := zw.CreateHeader(hdr)
164+
if err != nil {
165+
return fmt.Errorf("creating header for file %d: %s: %w", idx, file.Name(), err)
166+
}
167+
168+
// directories have no file body
169+
if file.IsDir() {
170+
return nil
171+
}
172+
if err := openAndCopyFile(file, w); err != nil {
173+
return fmt.Errorf("writing file %d: %s: %w", idx, file.Name(), err)
174+
}
175+
152176
return nil
153177
}
154178

@@ -159,10 +183,6 @@ func (z Zip) Archive(ctx context.Context, output io.Writer, files []File) error
159183
// with. Due to the nature of the zip archive format, if sourceArchive is not an io.Seeker
160184
// and io.ReaderAt, an error is returned.
161185
func (z Zip) Extract(ctx context.Context, sourceArchive io.Reader, pathsInArchive []string, handleFile FileHandler) error {
162-
if ctx == nil {
163-
ctx = context.Background()
164-
}
165-
166186
sra, ok := sourceArchive.(seekReaderAt)
167187
if !ok {
168188
return fmt.Errorf("input type must be an io.ReaderAt and io.Seeker because of zip format constraints")

0 commit comments

Comments
 (0)