-
Notifications
You must be signed in to change notification settings - Fork 458
/
tar.go
156 lines (124 loc) · 3.09 KB
/
tar.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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package archiver
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"slices"
"syscall"
multierror "github.com/hashicorp/go-multierror"
)
// Tar creates .tar archive and writes it to output for every item in paths channel.
func Tar(ctx context.Context, paths <-chan FileItem, output io.Writer) error {
tw := tar.NewWriter(output)
//nolint:errcheck
defer tw.Close()
var multiErr *multierror.Error
buf := make([]byte, 4096)
for fi := range paths {
if fi.Error != nil {
multiErr = multierror.Append(multiErr, fmt.Errorf("skipping %q: %s", fi.FullPath, fi.Error))
continue
}
err := processFile(ctx, tw, fi, buf)
if err != nil {
multiErr = multierror.Append(multiErr, fmt.Errorf("skipping %q: %s", fi.FullPath, err))
}
}
if err := tw.Close(); err != nil {
multiErr = multierror.Append(multiErr, err)
}
return multiErr.ErrorOrNil()
}
//nolint:gocyclo
func processFile(ctx context.Context, tw *tar.Writer, fi FileItem, buf []byte) error {
header, err := tar.FileInfoHeader(fi.FileInfo, fi.Link)
if err != nil {
// not supported by tar
return err
}
header.Name = fi.RelPath
if fi.FileInfo.IsDir() {
header.Name += string(os.PathSeparator)
}
skipData := false
switch header.Typeflag {
case tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeDir, tar.TypeFifo:
// no data for these types, move on
skipData = true
}
var r io.Reader
if !skipData {
var fp *os.File
fp, err = os.Open(fi.FullPath)
if err != nil {
return err
}
defer fp.Close() //nolint:errcheck
r = fp
}
if !skipData && header.Size == 0 {
// Linux reports /proc files as zero length, but they might have data,
// so we try to read limited amount of data from it to determine the size
var n int
n, err = r.Read(buf)
switch {
case err == io.EOF:
// file is empty for real
skipData = true
case err != nil:
// error reading from the file
if errors.Is(err, syscall.EINVAL) {
// some files are not supported by os.Open, e.g. /proc/sys/net/ipv4/conf/all/accept_local
skipData = true
} else {
return err
}
case n < len(buf):
header.Size = int64(n)
r = bytes.NewReader(slices.Clone(buf[:n]))
default:
// none matched so the file is bigger than we expected, ignore it and copy as zero size
skipData = true
}
}
err = tw.WriteHeader(header)
if err != nil {
return err
}
if skipData {
return nil
}
return archiveFile(ctx, tw, fi, r, buf)
}
func archiveFile(ctx context.Context, tw io.Writer, fi FileItem, r io.Reader, buf []byte) error {
for {
n, err := r.Read(buf)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
_, err = tw.Write(buf[:n])
if err != nil {
if err == tar.ErrWriteTooLong {
log.Printf("ignoring long write for %q", fi.FullPath)
return nil
}
return err
}
}
}