Repository navigation
Expand file tree
/
Copy patharchive.go
More file actions
336 lines (289 loc) · 9.35 KB
/
Copy patharchive.go
File metadata and controls
336 lines (289 loc) · 9.35 KB
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package filesystem
import (
"archive/tar"
"context"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/juju/ratelimit"
"github.com/klauspost/pgzip"
ignore "github.com/sabhiram/go-gitignore"
"github.com/pelican/wings/config"
"github.com/pelican/wings/internal/progress"
"github.com/pelican/wings/internal/ufs"
)
const memory = 4 * 1024
var pool = sync.Pool{
New: func() interface{} {
b := make([]byte, memory)
return b
},
}
// TarProgress .
type TarProgress struct {
*tar.Writer
p *progress.Progress
}
// NewTarProgress .
func NewTarProgress(w *tar.Writer, p *progress.Progress) *TarProgress {
if p != nil {
p.Writer = w
}
return &TarProgress{
Writer: w,
p: p,
}
}
// Write .
func (p *TarProgress) Write(v []byte) (int, error) {
if p.p == nil {
return p.Writer.Write(v)
}
return p.p.Write(v)
}
type Archive struct {
// Filesystem to create the archive with.
Filesystem *Filesystem
// Ignore is a gitignore string (most likely read from a file) of files to ignore
// from the archive.
Ignore string
// BaseDirectory .
BaseDirectory string
// Files specifies the files to archive, this takes priority over the Ignore
// option, if unspecified, all files in the BaseDirectory will be archived
// unless Ignore is set.
Files []string
// Progress wraps the writer of the archive to pass through the progress tracker.
Progress *progress.Progress
w *TarProgress
}
// Create creates an archive at dst with all the files defined in the
// included Files array.
//
// THIS IS UNSAFE TO USE IF `dst` IS PROVIDED BY A USER! ONLY USE THIS WITH
// CONTROLLED PATHS!
func (a *Archive) Create(ctx context.Context, dst string) error {
// Using os.OpenFile here is expected, as long as `dst` is not a user
// provided path.
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer f.Close()
// Select a writer based off of the WriteLimit configuration option. If there is no
// write limit, use the file as the writer.
var writer io.Writer
if writeLimit := int64(config.Get().System.Backups.WriteLimit * 1024 * 1024); writeLimit > 0 {
// Token bucket with a capacity of "writeLimit" MiB, adding "writeLimit" MiB/s
// and then wrap the file writer with the token bucket limiter.
writer = ratelimit.Writer(f, ratelimit.NewBucketWithRate(float64(writeLimit), writeLimit))
} else {
writer = f
}
return a.Stream(ctx, writer)
}
type walkFunc func(dirfd int, name, relative string, d ufs.DirEntry) error
// Stream streams the creation of the archive to the given writer.
func (a *Archive) Stream(ctx context.Context, w io.Writer) error {
if a.Filesystem == nil {
return errors.New("filesystem: archive.Filesystem is unset")
}
// The base directory may come with a prefixed `/`, strip it to prevent
// problems.
a.BaseDirectory = strings.TrimPrefix(a.BaseDirectory, "/")
if filesLen := len(a.Files); filesLen > 0 {
files := make([]string, filesLen)
for i, f := range a.Files {
if !strings.HasPrefix(f, a.Filesystem.Path()) {
files[i] = f
continue
}
files[i] = strings.TrimPrefix(strings.TrimPrefix(f, a.Filesystem.Path()), "/")
}
a.Files = files
}
// Choose which compression level to use based on the compression_level configuration option
var compressionLevel int
switch config.Get().System.Backups.CompressionLevel {
case "none":
compressionLevel = pgzip.NoCompression
case "best_compression":
compressionLevel = pgzip.BestCompression
default:
compressionLevel = pgzip.BestSpeed
}
// Create a new gzip writer around the file.
gw, _ := pgzip.NewWriterLevel(w, compressionLevel)
_ = gw.SetConcurrency(1<<20, 1)
defer gw.Close()
// Create a new tar writer around the gzip writer.
tw := tar.NewWriter(gw)
defer tw.Close()
a.w = NewTarProgress(tw, a.Progress)
fs := a.Filesystem.unixFS
// If we're specifically looking for only certain files, or have requested
// that certain files be ignored we'll update the callback function to reflect
// that request.
var callback walkFunc
if len(a.Files) == 0 && len(a.Ignore) > 0 {
i := ignore.CompileIgnoreLines(strings.Split(a.Ignore, "\n")...)
callback = a.callback(func(_ int, _, relative string, _ ufs.DirEntry) error {
if i.MatchesPath(relative) {
return SkipThis
}
return nil
})
} else if len(a.Files) > 0 {
callback = a.withFilesCallback()
} else {
callback = a.callback()
}
// Open the base directory we were provided.
dirfd, name, closeFd, err := fs.SafePath(a.BaseDirectory)
defer closeFd()
if err != nil {
return err
}
// Recursively walk the base directory.
return fs.WalkDirat(dirfd, name, func(dirfd int, name, relative string, d ufs.DirEntry, err error) error {
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
return callback(dirfd, name, relative, d)
}
})
}
// Callback function used to determine if a given file should be included in the archive
// being generated.
func (a *Archive) callback(opts ...walkFunc) walkFunc {
// Get the base directory we need to strip when walking.
//
// This is important as when we are walking, the last part of the base directory
// is present on all the paths we walk.
var base string
if a.BaseDirectory != "" {
base = filepath.Base(a.BaseDirectory) + "/"
}
return func(dirfd int, name, relative string, d ufs.DirEntry) error {
// Skip directories because we are walking them recursively.
if d.IsDir() {
return nil
}
// If base isn't empty, strip it from the relative path. This fixes an
// issue when creating an archive starting from a nested directory.
if base != "" {
relative = strings.TrimPrefix(relative, base)
}
// Call the additional options passed to this callback function. If any of them return
// a non-nil error we will exit immediately.
for _, opt := range opts {
if err := opt(dirfd, name, relative, d); err != nil {
if err == SkipThis {
return nil
}
return err
}
}
// Add the file to the archive, if it is nested in a directory,
// the directory will be automatically "created" in the archive.
return a.addToArchive(dirfd, name, relative, d)
}
}
var SkipThis = errors.New("skip this")
// Pushes only files defined in the Files key to the final archive.
func (a *Archive) withFilesCallback() walkFunc {
return a.callback(func(_ int, _, relative string, _ ufs.DirEntry) error {
for _, f := range a.Files {
// Allow exact file matches, otherwise check if file is within a parent directory.
//
// The slashes are added in the prefix checks to prevent partial name matches from being
// included in the archive.
if f != relative && !strings.HasPrefix(strings.TrimSuffix(relative, "/")+"/", strings.TrimSuffix(f, "/")+"/") {
continue
}
// Once we have a match return a nil value here so that the loop stops and the
// call to this function will correctly include the file in the archive. If there
// are no matches we'll never make it to this line, and the final error returned
// will be the ufs.SkipDir error.
return nil
}
return SkipThis
})
}
// Adds a given file path to the final archive being created.
func (a *Archive) addToArchive(dirfd int, name, relative string, entry ufs.DirEntry) error {
s, err := entry.Info()
if err != nil {
if errors.Is(err, ufs.ErrNotExist) {
return nil
}
return errors.WrapIff(err, "failed executing os.Lstat on '%s'", name)
}
// Skip socket files as they are unsupported by archive/tar.
// Error will come from tar#FileInfoHeader: "archive/tar: sockets not supported"
if s.Mode()&fs.ModeSocket != 0 {
return nil
}
var target string
if s.Mode()&fs.ModeSymlink != 0 {
target, err = a.Filesystem.unixFS.Readlinkat(dirfd, name)
if err != nil {
if !errors.Is(err, ufs.ErrNotExist) {
log.WithField("name", name).WithField("readlink_err", err.Error()).Warn("failed reading symlink for target path; skipping...")
}
return nil
}
}
// Get the tar FileInfoHeader in order to add the file to the archive.
header, err := tar.FileInfoHeader(s, filepath.ToSlash(target))
if err != nil {
return errors.WrapIff(err, "failed to get tar#FileInfoHeader for '%s'", name)
}
// Fix the header name if the file is not a symlink.
if s.Mode()&fs.ModeSymlink == 0 {
header.Name = relative
}
// Write the tar FileInfoHeader to the archive.
if err := a.w.WriteHeader(header); err != nil {
return errors.WrapIff(err, "failed to write tar#FileInfoHeader for '%s'", name)
}
// If the size of the file is less than 1 (most likely for symlinks), skip writing the file.
if header.Size < 1 {
return nil
}
// If the buffer size is larger than the file size, create a smaller buffer to hold the file.
var buf []byte
if header.Size < memory {
buf = make([]byte, header.Size)
} else {
// Get a fixed-size buffer from the pool to save on allocations.
buf = pool.Get().([]byte)
defer func() {
buf = make([]byte, memory)
pool.Put(buf)
}()
}
// Open the file.
f, err := a.Filesystem.unixFS.OpenFileat(dirfd, name, ufs.O_RDONLY, 0)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return errors.WrapIff(err, "failed to open '%s' for copying", header.Name)
}
defer f.Close()
// Copy the file's contents to the archive using our buffer.
if _, err := io.CopyBuffer(a.w, io.LimitReader(f, header.Size), buf); err != nil {
return errors.WrapIff(err, "failed to copy '%s' to archive", header.Name)
}
return nil
}