-
Notifications
You must be signed in to change notification settings - Fork 1
/
os.go
314 lines (253 loc) · 6.88 KB
/
os.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
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
package main
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"context"
"encoding/hex"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/h2non/filetype"
"github.com/zeebo/blake3"
)
var (
pkgDir = getEnv("VIN_PATH", "/etc/vinyl/pkg")
configFile = getEnv("VIN_CONFIG", "/etc/vinyl/vin.toml")
cacheDir = getEnv("VIN_CACHE", "/var/cache/vinyl/vin/packages")
sockAddr = getEnv("VIN_SOCKET_ADDR", "/var/run/vin.sock")
stateDB = getEnv("VIN_STATE_DB", "/etc/vinyl/vin.db")
svcDir = getEnv("VINIT_SVC_DIR", "/etc/vinit/services")
)
// ChanWriter wraps a string channel, and implements the io.Writer
// interface in order to attach it to the Stdout and Stderr of a process
type ChanWriter chan string
// Write implements the io.Writer interface; it writes p to the underlying
// channel as a string
func (cw ChanWriter) Write(p []byte) (n int, err error) {
cw <- string(p)
return len(p), nil
}
func getEnv(key, def string) string {
v, ok := os.LookupEnv(key)
if !ok {
v = def
}
return v
}
// checksum takes a filename, opens it, and generates a Blake3 sum from it
//
// Why blake3? Because it's fast, (supposedly) collision free, and is used
// little enough that package publishers will be forced to generate checksums
// solely for vin, thus ensuring the checksums are correct.
//
// This is as opposed to copy/pasting any old checksum from any old source.
func checksum(fn string) (sum string, err error) {
f, err := os.Open(fn)
if err != nil {
return
}
defer f.Close()
h := blake3.New()
_, err = io.Copy(h, f)
if err != nil {
return
}
outB := make([]byte, 0)
sumB := h.Sum(outB)
sum = hex.EncodeToString(sumB)
return
}
// download a file, saving to fn
//
// this solution is, in part, taken from https://stackoverflow.com/a/33853856
func download(fn, url string) (err error) {
out, err := os.Create(fn)
if err != nil {
return
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("downloading %s: expected status 200, received %s", url, resp.Status)
}
_, err = io.Copy(out, resp.Body)
return
}
// decompress opens a tarball and decompresses it according to
// which ever method was used to compress it
func decompress(data *os.File) (decompressor io.Reader, err error) {
// Read header, determine compression method
head := make([]byte, 261)
data.Read(head)
ty, err := filetype.Match(head)
if err != nil {
return
}
// Go back to the start
data.Seek(0, os.SEEK_SET)
switch ty.MIME.Value {
case "application/gzip":
decompressor, err = gzip.NewReader(data)
case "application/x-bzip2":
decompressor = bzip2.NewReader(data)
default:
err = fmt.Errorf("untar: tarball is of (unsupported) type %q", ty.MIME.Value)
}
return
}
// untar will untar a tarball
//
// this solution is, in part, taken from https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07
func untar(fn, dir string) (err error) {
// detect filetype
f, err := os.Open(fn)
if err != nil {
return
}
defer f.Close()
decompressor, err := decompress(f)
if err != nil {
return
}
switch decompressor.(type) {
case *gzip.Reader:
defer decompressor.(*gzip.Reader).Close()
}
tr := tar.NewReader(decompressor)
return decompressLoop(tr, dir)
}
// decompressLoop iterates over a tar reader until either the tarball is
// untarred, or an error occurs
func decompressLoop(tr *tar.Reader, dest string) (err error) {
var header *tar.Header
for {
header, err = tr.Next()
switch {
case err == io.EOF:
return nil
case err != nil:
return
case header == nil:
continue
}
// the target location where the dir/file should be created
target := filepath.Join(dest, header.Name)
// ensure the parent directory exists in situations where a single file,
// with missing toplevel dir, is compressed.
//
// This seemingly weird case exists in some tarballs in our test cases, so
// it can definitely happen. I'd love to understand more /shrug
err = os.MkdirAll(filepath.Dir(target), 0755)
if err != nil {
return
}
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
return err
}
f.Close()
case tar.TypeLink:
// remove target if it exists.
//
// ignoring the error is fine here; if there's an error
// we'll see it when we try to link anyway /shrug
os.Remove(target)
err = os.Link(filepath.Join(dest, header.Linkname), target)
if err != nil {
return
}
case tar.TypeSymlink:
// see comment for the tar.TypeLink case above;
os.Remove(target)
// we don't need to worry about prefixing synlinks. Infact,
// we probably don't want that at all. symlinks can point to
// files that don't exist, and probably want to be more flexible
// for things like relative links anyway
err = os.Symlink(header.Linkname, target)
if err != nil {
return
}
}
}
}
func execute(dir, command string, skipEnv bool, output chan string) (err error) {
cmdSlice := strings.Fields(command)
var args []string
switch len(cmdSlice) {
case 0:
return fmt.Errorf("execute: %q is empty, or cannot be split", command)
case 1:
// NOP; in this case leave args as empty
default:
args = cmdSlice[1:]
}
cmd := exec.CommandContext(context.Background(), cmdSlice[0], args...)
cmd.Dir = dir
if !skipEnv {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("CFLAGS=%s", cfg.CFlags), fmt.Sprintf("CXXFLAGS=%s", cfg.CXXFlags))
}
outputWriter := ChanWriter(output)
cmd.Stdout = outputWriter
cmd.Stderr = outputWriter
return cmd.Run()
}
func installServiceDir(src string) (err error) {
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
name := strings.TrimPrefix(path, src)
dst := filepath.Join(svcDir, filepath.Base(src), name)
if info.IsDir() {
return os.MkdirAll(dst, info.Mode())
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
l, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
// delete destination first; otherwise os.Symlink
// will error.
//
// Ignore the error; the only pertinent error is
// that the destination does not exist (which we don't
// care about)- anything else will be caught elsewhere
os.Remove(dst)
return os.Symlink(l, dst)
}
source, err := os.Open(path)
if err != nil {
return err
}
defer source.Close()
destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, info.Mode())
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return err
})
}