-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathzip.go
207 lines (198 loc) · 5.46 KB
/
zip.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package modmake
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"strings"
)
// ZipArchive represents a Runner that performs operations on a tar archive that uses gzip compression.
// The Runner is created with Zip.
type ZipArchive struct {
err error
path PathString
addFiles map[PathString]string
}
// Zip will create a new ZipArchive to contextualize follow-on operations that act on a zip file.
// Adding a ".zip" suffix to the location path string is recommended, but not required.
func Zip(location PathString) *ZipArchive {
if len(location) == 0 {
panic("empty location")
}
return &ZipArchive{
path: location,
addFiles: map[PathString]string{},
}
}
// AddFile adds the referenced file with the same archive path as what is given.
// The archive path will be converted to slash format.
func (z *ZipArchive) AddFile(sourcePath PathString) *ZipArchive {
z.AddFileWithPath(sourcePath, sourcePath)
return z
}
// AddFileWithPath adds the referenced file with an archive path specified.
// The archive path will be converted to slash format.
func (z *ZipArchive) AddFileWithPath(sourcePath, archivePath PathString) *ZipArchive {
if z.err != nil {
return z
}
if len(sourcePath) == 0 {
panic("empty source path")
}
if len(archivePath) == 0 {
panic("empty target path")
}
z.addFiles[sourcePath] = archivePath.ToSlash()
return z
}
// Create will return a Runner that creates a new zip file with the given files loaded.
// If a file with the given name already exists, then it will be truncated first.
// Ensure that all files referenced with AddFile (or AddFileWithPath) and directories exist before running this Runner, because it doesn't try to create them.
func (z *ZipArchive) Create() Task {
runner := Task(func(ctx context.Context) error {
zipFile, err := z.path.Create()
if err != nil {
return err
}
defer func() {
_ = zipFile.Close()
}()
zw := zip.NewWriter(zipFile)
defer func() {
_ = zw.Close()
}()
if err := z.writeFilesToZipArchive(ctx, zw); err != nil {
return err
}
return nil
})
return ContextAware(runner).Run
}
// Update will return a Runner that creates a new zip file with the given files loaded.
// If a file with the given name already exists, then it will be updated.
// If a file with the given name does not exist, then this Runner will return an error.
// Ensure that all files referenced with AddFile (or AddFileWithPath) and directories exist before running this Runner, because it doesn't try to create them.
func (z *ZipArchive) Update() Task {
runner := Task(func(ctx context.Context) error {
zipFile, err := z.path.OpenFile(os.O_RDWR, 0644)
if err != nil {
return err
}
defer func() {
_ = zipFile.Close()
}()
zw := zip.NewWriter(zipFile)
defer func() {
_ = zw.Close()
}()
if err := z.writeFilesToZipArchive(ctx, zw); err != nil {
return err
}
return nil
})
return ContextAware(runner).Run
}
func (z *ZipArchive) writeFilesToZipArchive(ctx context.Context, zw *zip.Writer) error {
for source, target := range z.addFiles {
select {
case <-ctx.Done():
return ctx.Err()
default:
source, target := source, target
err := func() error {
f, err := source.Open()
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
t, err := zw.Create(target)
if err != nil {
return err
}
_, err = io.Copy(t, f)
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
}
}
return nil
}
// Extract will extract the named zip archive to the given directory.
// Any errors encountered while doing so will be immediately returned.
func (z *ZipArchive) Extract(extractDir PathString) Task {
runner := Task(func(ctx context.Context) error {
src, err := z.path.Open()
if err != nil {
return fmt.Errorf("unable to open zip archive: %w", err)
}
defer func() {
_ = src.Close()
}()
fi, err := z.path.Stat()
if err != nil {
return fmt.Errorf("unable to get file information for the source zip file: %w", err)
}
err = extractDir.MkdirAll(0755)
if err != nil {
return fmt.Errorf("unable to create extraction directory: %w", err)
}
zr, err := zip.NewReader(src, fi.Size())
if err != nil {
return fmt.Errorf("failed to open '%s' for reading: %w", z.path, err)
}
for _, f := range zr.File {
f := f
select {
case <-ctx.Done():
return ctx.Err()
default:
err := func() error {
output := extractDir.Join(f.Name)
if strings.HasSuffix(output.String(), "/") {
err := output.MkdirAll(0755)
if err != nil {
return fmt.Errorf("failed to create parent directory '%s': %w", output, err)
}
return nil
}
outputDir := output.Dir()
if err := outputDir.MkdirAll(0755); err != nil {
return fmt.Errorf("failed to make parent directory for file '%s' at '%s': %w", f.Name, outputDir, err)
}
zipFile, err := f.Open()
if err != nil {
return fmt.Errorf("failed to open compressed file '%s' for reading: %w", f.Name, err)
}
defer func() {
_ = zipFile.Close()
}()
out, err := output.Create()
if err != nil {
return fmt.Errorf("failed to create file '%s': %w", output, err)
}
defer func() {
_ = out.Close()
}()
_, err = io.Copy(out, zipFile)
if err != nil {
return fmt.Errorf("failed to extract '%s': %w", output, err)
}
return nil
}()
if err != nil {
return err
}
}
}
return nil
})
return ContextAware(runner).Run
}