Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/downloader/downloader.go
2655 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package downloader
5
6
import (
7
"bytes"
8
"context"
9
"crypto/sha256"
10
"errors"
11
"fmt"
12
"io"
13
"net/http"
14
"os"
15
"os/exec"
16
"path"
17
"path/filepath"
18
"strings"
19
"sync/atomic"
20
"time"
21
22
"github.com/cheggaaa/pb/v3"
23
"github.com/containerd/continuity/fs"
24
"github.com/opencontainers/go-digest"
25
"github.com/sirupsen/logrus"
26
27
"github.com/lima-vm/lima/v2/pkg/httpclientutil"
28
"github.com/lima-vm/lima/v2/pkg/localpathutil"
29
"github.com/lima-vm/lima/v2/pkg/lockutil"
30
"github.com/lima-vm/lima/v2/pkg/progressbar"
31
)
32
33
// HideProgress is used only for testing.
34
var HideProgress bool
35
36
// hideBar is used only for testing.
37
func hideBar(bar *progressbar.ProgressBar) {
38
bar.Set(pb.Static, true)
39
}
40
41
type Status = string
42
43
const (
44
StatusUnknown Status = ""
45
StatusDownloaded Status = "downloaded"
46
StatusSkipped Status = "skipped"
47
StatusUsedCache Status = "used-cache"
48
)
49
50
type Result struct {
51
Status Status
52
CachePath string // "/Users/foo/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>/data"
53
LastModified time.Time
54
ContentType string
55
ValidatedDigest bool
56
}
57
58
type options struct {
59
cacheDir string // default: empty (disables caching)
60
decompress bool // default: false (keep compression)
61
description string // default: url
62
expectedDigest digest.Digest
63
}
64
65
func (o *options) apply(opts []Opt) error {
66
for _, f := range opts {
67
if err := f(o); err != nil {
68
return err
69
}
70
}
71
return nil
72
}
73
74
type Opt func(*options) error
75
76
// WithCache enables caching using filepath.Join(os.UserCacheDir(), "lima") as the cache dir.
77
func WithCache() Opt {
78
return func(o *options) error {
79
ucd, err := os.UserCacheDir()
80
if err != nil {
81
return err
82
}
83
cacheDir := filepath.Join(ucd, "lima")
84
return WithCacheDir(cacheDir)(o)
85
}
86
}
87
88
// WithCacheDir enables caching using the specified dir.
89
// Empty value disables caching.
90
func WithCacheDir(cacheDir string) Opt {
91
return func(o *options) error {
92
o.cacheDir = cacheDir
93
return nil
94
}
95
}
96
97
// WithDescription adds a user description of the download.
98
func WithDescription(description string) Opt {
99
return func(o *options) error {
100
o.description = description
101
return nil
102
}
103
}
104
105
// WithDecompress decompress the download from the cache.
106
func WithDecompress(decompress bool) Opt {
107
return func(o *options) error {
108
o.decompress = decompress
109
return nil
110
}
111
}
112
113
// WithExpectedDigest is used to validate the downloaded file against the expected digest.
114
//
115
// The digest is not verified in the following cases:
116
// - The digest was not specified.
117
// - The file already exists in the local target path.
118
//
119
// When the `data` file exists in the cache dir with `<ALGO>.digest` file,
120
// the digest is verified by comparing the content of `<ALGO>.digest` with the expected
121
// digest string. So, the actual digest of the `data` file is not computed.
122
func WithExpectedDigest(expectedDigest digest.Digest) Opt {
123
return func(o *options) error {
124
if expectedDigest != "" {
125
if err := expectedDigest.Validate(); err != nil {
126
return err
127
}
128
}
129
130
o.expectedDigest = expectedDigest
131
return nil
132
}
133
}
134
135
func readFile(path string) string {
136
if path == "" {
137
return ""
138
}
139
if _, err := os.Stat(path); err != nil {
140
return ""
141
}
142
b, err := os.ReadFile(path)
143
if err != nil {
144
return ""
145
}
146
return string(b)
147
}
148
149
func readTime(path string) time.Time {
150
if path == "" {
151
return time.Time{}
152
}
153
if _, err := os.Stat(path); err != nil {
154
return time.Time{}
155
}
156
b, err := os.ReadFile(path)
157
if err != nil {
158
return time.Time{}
159
}
160
t, err := time.Parse(http.TimeFormat, string(b))
161
if err != nil {
162
return time.Time{}
163
}
164
return t
165
}
166
167
// Download downloads the remote resource into the local path.
168
//
169
// Download caches the remote resource if WithCache or WithCacheDir option is specified.
170
// Local files are not cached.
171
//
172
// When the local path already exists, Download returns Result with StatusSkipped.
173
// (So, the local path cannot be set to /dev/null for "caching only" mode.)
174
//
175
// The local path can be an empty string for "caching only" mode.
176
func Download(ctx context.Context, local, remote string, opts ...Opt) (*Result, error) {
177
var o options
178
if err := o.apply(opts); err != nil {
179
return nil, err
180
}
181
182
var localPath string
183
if local == "" {
184
if o.cacheDir == "" {
185
return nil, errors.New("caching-only mode requires the cache directory to be specified")
186
}
187
} else {
188
var err error
189
localPath, err = canonicalLocalPath(local)
190
if err != nil {
191
return nil, err
192
}
193
if _, err := os.Stat(localPath); err == nil {
194
logrus.Debugf("file %q already exists, skipping downloading from %q (and skipping digest validation)", localPath, remote)
195
res := &Result{
196
Status: StatusSkipped,
197
ValidatedDigest: false,
198
}
199
return res, nil
200
} else if !errors.Is(err, os.ErrNotExist) {
201
return nil, err
202
}
203
204
localPathDir := filepath.Dir(localPath)
205
if err := os.MkdirAll(localPathDir, 0o755); err != nil {
206
return nil, err
207
}
208
}
209
210
ext := path.Ext(remote)
211
if IsLocal(remote) {
212
if err := copyLocal(ctx, localPath, remote, ext, o.decompress, o.description, o.expectedDigest); err != nil {
213
return nil, err
214
}
215
res := &Result{
216
Status: StatusDownloaded,
217
ValidatedDigest: o.expectedDigest != "",
218
}
219
return res, nil
220
}
221
222
if o.cacheDir == "" {
223
if err := downloadHTTP(ctx, localPath, "", "", remote, o.description, o.expectedDigest); err != nil {
224
return nil, err
225
}
226
res := &Result{
227
Status: StatusDownloaded,
228
ValidatedDigest: o.expectedDigest != "",
229
}
230
return res, nil
231
}
232
233
shad := cacheDirectoryPath(o.cacheDir, remote, o.decompress)
234
if err := os.MkdirAll(shad, 0o700); err != nil {
235
return nil, err
236
}
237
238
var res *Result
239
err := lockutil.WithDirLock(shad, func() error {
240
var err error
241
res, err = getCached(ctx, localPath, remote, o)
242
if err != nil {
243
return err
244
}
245
if res != nil {
246
return nil
247
}
248
res, err = fetch(ctx, localPath, remote, o)
249
return err
250
})
251
return res, err
252
}
253
254
// getCached tries to copy the file from the cache to local path. Return result,
255
// nil if the file was copied, nil, nil if the file is not in the cache or the
256
// cache needs update, or nil, error on fatal error.
257
func getCached(ctx context.Context, localPath, remote string, o options) (*Result, error) {
258
shad := cacheDirectoryPath(o.cacheDir, remote, o.decompress)
259
shadData := filepath.Join(shad, "data")
260
shadTime := filepath.Join(shad, "time")
261
shadType := filepath.Join(shad, "type")
262
shadDigest, err := cacheDigestPath(shad, o.expectedDigest)
263
if err != nil {
264
return nil, err
265
}
266
if _, err := os.Stat(shadData); err != nil {
267
return nil, nil
268
}
269
ext := path.Ext(remote)
270
logrus.Debugf("file %q is cached as %q", localPath, shadData)
271
if _, err := os.Stat(shadDigest); err == nil {
272
logrus.Debugf("Comparing digest %q with the cached digest file %q, not computing the actual digest of %q",
273
o.expectedDigest, shadDigest, shadData)
274
if err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {
275
return nil, err
276
}
277
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, "", ""); err != nil {
278
return nil, err
279
}
280
} else {
281
if match, lmCached, lmRemote, err := matchLastModified(ctx, shadTime, remote); err != nil {
282
logrus.WithError(err).Info("Failed to retrieve last-modified for cached digest-less image; using cached image.")
283
} else if match {
284
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, o.description, o.expectedDigest); err != nil {
285
return nil, err
286
}
287
} else {
288
logrus.Infof("Re-downloading digest-less image: last-modified mismatch (cached: %q, remote: %q)", lmCached, lmRemote)
289
return nil, nil
290
}
291
}
292
res := &Result{
293
Status: StatusUsedCache,
294
CachePath: shadData,
295
LastModified: readTime(shadTime),
296
ContentType: readFile(shadType),
297
ValidatedDigest: o.expectedDigest != "",
298
}
299
return res, nil
300
}
301
302
// fetch downloads remote to the cache and copy the cached file to local path.
303
func fetch(ctx context.Context, localPath, remote string, o options) (*Result, error) {
304
shad := cacheDirectoryPath(o.cacheDir, remote, o.decompress)
305
shadData := filepath.Join(shad, "data")
306
shadTime := filepath.Join(shad, "time")
307
shadType := filepath.Join(shad, "type")
308
shadDigest, err := cacheDigestPath(shad, o.expectedDigest)
309
if err != nil {
310
return nil, err
311
}
312
ext := path.Ext(remote)
313
shadURL := filepath.Join(shad, "url")
314
if err := os.WriteFile(shadURL, []byte(remote), 0o644); err != nil {
315
return nil, err
316
}
317
if err := downloadHTTP(ctx, shadData, shadTime, shadType, remote, o.description, o.expectedDigest); err != nil {
318
return nil, err
319
}
320
if shadDigest != "" && o.expectedDigest != "" {
321
if err := os.WriteFile(shadDigest, []byte(o.expectedDigest.String()), 0o644); err != nil {
322
return nil, err
323
}
324
}
325
// no need to pass the digest to copyLocal(), as we already verified the digest
326
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, "", ""); err != nil {
327
return nil, err
328
}
329
res := &Result{
330
Status: StatusDownloaded,
331
CachePath: shadData,
332
LastModified: readTime(shadTime),
333
ContentType: readFile(shadType),
334
ValidatedDigest: o.expectedDigest != "",
335
}
336
return res, nil
337
}
338
339
// Cached checks if the remote resource is in the cache.
340
//
341
// Download caches the remote resource if WithCache or WithCacheDir option is specified.
342
// Local files are not cached.
343
//
344
// When the cache path already exists, Cached returns Result with StatusUsedCache.
345
func Cached(remote string, opts ...Opt) (*Result, error) {
346
var o options
347
if err := o.apply(opts); err != nil {
348
return nil, err
349
}
350
if o.cacheDir == "" {
351
return nil, errors.New("caching-only mode requires the cache directory to be specified")
352
}
353
if IsLocal(remote) {
354
return nil, errors.New("local files are not cached")
355
}
356
357
shad := cacheDirectoryPath(o.cacheDir, remote, o.decompress)
358
shadData := filepath.Join(shad, "data")
359
shadTime := filepath.Join(shad, "time")
360
shadType := filepath.Join(shad, "type")
361
shadDigest, err := cacheDigestPath(shad, o.expectedDigest)
362
if err != nil {
363
return nil, err
364
}
365
366
// Checking if data file exists is safe without locking.
367
if _, err := os.Stat(shadData); err != nil {
368
return nil, err
369
}
370
371
// But validating the digest or the data file must take the lock to avoid races
372
// with parallel downloads.
373
if err := os.MkdirAll(shad, 0o700); err != nil {
374
return nil, err
375
}
376
err = lockutil.WithDirLock(shad, func() error {
377
if _, err := os.Stat(shadDigest); err != nil {
378
if err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {
379
return err
380
}
381
} else {
382
if err := validateLocalFileDigest(shadData, o.expectedDigest); err != nil {
383
return err
384
}
385
}
386
return nil
387
})
388
if err != nil {
389
return nil, err
390
}
391
392
res := &Result{
393
Status: StatusUsedCache,
394
CachePath: shadData,
395
LastModified: readTime(shadTime),
396
ContentType: readFile(shadType),
397
ValidatedDigest: o.expectedDigest != "",
398
}
399
return res, nil
400
}
401
402
// cacheDirectoryPath returns the cache subdirectory path.
403
// - "url" file contains the url
404
// - "data" file contains the data
405
// - "time" file contains the time (Last-Modified header)
406
// - "type" file contains the type (Content-Type header)
407
func cacheDirectoryPath(cacheDir, remote string, decompress bool) string {
408
return filepath.Join(cacheDir, "download", "by-url-sha256", CacheKey(remote, decompress))
409
}
410
411
// cacheDigestPath returns the cache digest file path.
412
// - "<ALGO>.digest" contains the digest
413
func cacheDigestPath(shad string, expectedDigest digest.Digest) (string, error) {
414
shadDigest := ""
415
if expectedDigest != "" {
416
algo := expectedDigest.Algorithm().String()
417
if strings.Contains(algo, "/") || strings.Contains(algo, "\\") {
418
return "", fmt.Errorf("invalid digest algorithm %q", algo)
419
}
420
shadDigest = filepath.Join(shad, algo+".digest")
421
}
422
return shadDigest, nil
423
}
424
425
func IsLocal(s string) bool {
426
return !strings.Contains(s, "://") || strings.HasPrefix(s, "file://")
427
}
428
429
// canonicalLocalPath canonicalizes the local path string.
430
// - Make sure the file has no scheme, or the `file://` scheme
431
// - If it has the `file://` scheme, strip the scheme and make sure the filename is absolute
432
// - Expand a leading `~`, or convert relative to absolute name
433
func canonicalLocalPath(s string) (string, error) {
434
if s == "" {
435
return "", errors.New("got empty path")
436
}
437
if !IsLocal(s) {
438
return "", fmt.Errorf("got non-local path: %q", s)
439
}
440
if res, ok := strings.CutPrefix(s, "file://"); ok {
441
if !filepath.IsAbs(res) {
442
return "", fmt.Errorf("got non-absolute path %q", res)
443
}
444
return res, nil
445
}
446
return localpathutil.Expand(s)
447
}
448
449
func copyLocal(ctx context.Context, dst, src, ext string, decompress bool, description string, expectedDigest digest.Digest) error {
450
srcPath, err := canonicalLocalPath(src)
451
if err != nil {
452
return err
453
}
454
455
if expectedDigest != "" {
456
logrus.Debugf("verifying digest of local file %q (%s)", srcPath, expectedDigest)
457
}
458
if err := validateLocalFileDigest(srcPath, expectedDigest); err != nil {
459
return err
460
}
461
462
if dst == "" {
463
// empty dst means caching-only mode
464
return nil
465
}
466
dstPath, err := canonicalLocalPath(dst)
467
if err != nil {
468
return err
469
}
470
if decompress {
471
command := decompressor(ext)
472
if command != "" {
473
return decompressLocal(ctx, command, dstPath, srcPath, ext, description)
474
}
475
commandByMagic := decompressorByMagic(srcPath)
476
if commandByMagic != "" {
477
return decompressLocal(ctx, commandByMagic, dstPath, srcPath, ext, description)
478
}
479
}
480
// TODO: progress bar for copy
481
return fs.CopyFile(dstPath, srcPath)
482
}
483
484
func decompressor(ext string) string {
485
switch ext {
486
case ".gz":
487
return "gzip"
488
case ".bz2":
489
return "bzip2"
490
case ".xz":
491
return "xz"
492
case ".zst":
493
return "zstd"
494
default:
495
return ""
496
}
497
}
498
499
func decompressorByMagic(file string) string {
500
f, err := os.Open(file)
501
if err != nil {
502
return ""
503
}
504
defer f.Close()
505
header := make([]byte, 6)
506
if _, err := f.Read(header); err != nil {
507
return ""
508
}
509
if _, err := f.Seek(0, io.SeekStart); err != nil {
510
return ""
511
}
512
if bytes.HasPrefix(header, []byte{0x1f, 0x8b}) {
513
return "gzip"
514
}
515
if bytes.HasPrefix(header, []byte{0x42, 0x5a}) {
516
return "bzip2"
517
}
518
if bytes.HasPrefix(header, []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}) {
519
return "xz"
520
}
521
if bytes.HasPrefix(header, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
522
return "zstd"
523
}
524
return ""
525
}
526
527
func decompressLocal(ctx context.Context, decompressCmd, dst, src, ext, description string) error {
528
logrus.Infof("decompressing %s with %v", ext, decompressCmd)
529
530
st, err := os.Stat(src)
531
if err != nil {
532
return err
533
}
534
bar, err := progressbar.New(st.Size())
535
if err != nil {
536
return err
537
}
538
if HideProgress {
539
hideBar(bar)
540
}
541
542
in, err := os.Open(src)
543
if err != nil {
544
return err
545
}
546
defer in.Close()
547
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644)
548
if err != nil {
549
return err
550
}
551
defer out.Close()
552
buf := new(bytes.Buffer)
553
cmd := exec.CommandContext(ctx, decompressCmd, "-d") // -d --decompress
554
cmd.Stdin = bar.NewProxyReader(in)
555
cmd.Stdout = out
556
cmd.Stderr = buf
557
if !HideProgress {
558
if description == "" {
559
description = filepath.Base(src)
560
}
561
logrus.Infof("Decompressing %s\n", description)
562
}
563
bar.Start()
564
err = cmd.Run()
565
if err != nil {
566
var exitErr *exec.ExitError
567
if errors.As(err, &exitErr) {
568
exitErr.Stderr = buf.Bytes()
569
}
570
}
571
bar.Finish()
572
return err
573
}
574
575
func validateCachedDigest(shadDigest string, expectedDigest digest.Digest) error {
576
if expectedDigest == "" {
577
return nil
578
}
579
shadDigestB, err := os.ReadFile(shadDigest)
580
if err != nil {
581
return err
582
}
583
shadDigestS := strings.TrimSpace(string(shadDigestB))
584
if shadDigestS != expectedDigest.String() {
585
return fmt.Errorf("expected digest %q, got %q", expectedDigest, shadDigestS)
586
}
587
return nil
588
}
589
590
func validateLocalFileDigest(localPath string, expectedDigest digest.Digest) error {
591
if localPath == "" {
592
return errors.New("validateLocalFileDigest: got empty localPath")
593
}
594
if expectedDigest == "" {
595
return nil
596
}
597
algo := expectedDigest.Algorithm()
598
if !algo.Available() {
599
return fmt.Errorf("expected digest algorithm %q is not available", algo)
600
}
601
r, err := os.Open(localPath)
602
if err != nil {
603
return err
604
}
605
defer r.Close()
606
actualDigest, err := algo.FromReader(r)
607
if err != nil {
608
return err
609
}
610
if actualDigest != expectedDigest {
611
return fmt.Errorf("expected digest %q, got %q", expectedDigest, actualDigest)
612
}
613
return nil
614
}
615
616
// mathLastModified takes params:
617
// - ctx: context for calling httpclientutil.Head
618
// - lastModifiedPath: path of the cached last-modified time file
619
// - url: URL to fetch the last-modified time
620
//
621
// returns:
622
// - matched: whether the last-modified time matches
623
// - lmCached: last-modified time string from the lastModifiedPath
624
// - lmRemote: last-modified time string from the URL
625
// - err: error if fetching the last-modified time from the URL fails
626
func matchLastModified(ctx context.Context, lastModifiedPath, url string) (matched bool, lmCached, lmRemote string, err error) {
627
lmCached = readFile(lastModifiedPath)
628
if lmCached == "" {
629
return false, "<not cached>", "<not checked>", nil
630
}
631
resp, err := httpclientutil.Head(ctx, http.DefaultClient, url)
632
if err != nil {
633
return false, lmCached, "<failed to fetch remote>", err
634
}
635
defer resp.Body.Close()
636
lmRemote = resp.Header.Get("Last-Modified")
637
if lmRemote == "" {
638
return false, lmCached, "<missing Last-Modified header>", nil
639
}
640
lmCachedTime, errParsingCachedTime := time.Parse(http.TimeFormat, lmCached)
641
lmRemoteTime, errParsingRemoteTime := time.Parse(http.TimeFormat, lmRemote)
642
if errParsingCachedTime != nil && errParsingRemoteTime != nil {
643
// both time strings are failed to parse, so compare them as strings
644
return lmCached == lmRemote, lmCached, lmRemote, nil
645
} else if errParsingCachedTime == nil && errParsingRemoteTime == nil {
646
// both time strings are successfully parsed, so compare them as times
647
return lmRemoteTime.Equal(lmCachedTime), lmCached, lmRemote, nil
648
}
649
// ignore parsing errors for either time string and assume they are different
650
return false, lmCached, lmRemote, nil
651
}
652
653
func downloadHTTP(ctx context.Context, localPath, lastModified, contentType, url, description string, expectedDigest digest.Digest) error {
654
if localPath == "" {
655
return errors.New("downloadHTTP: got empty localPath")
656
}
657
logrus.Debugf("downloading %q into %q", url, localPath)
658
659
resp, err := httpclientutil.Get(ctx, http.DefaultClient, url)
660
if err != nil {
661
return err
662
}
663
if lastModified != "" {
664
lm := resp.Header.Get("Last-Modified")
665
if err := os.WriteFile(lastModified, []byte(lm), 0o644); err != nil {
666
return err
667
}
668
}
669
if contentType != "" {
670
ct := resp.Header.Get("Content-Type")
671
if err := os.WriteFile(contentType, []byte(ct), 0o644); err != nil {
672
return err
673
}
674
}
675
defer resp.Body.Close()
676
bar, err := progressbar.New(resp.ContentLength)
677
if err != nil {
678
return err
679
}
680
if HideProgress {
681
hideBar(bar)
682
}
683
684
localPathTmp := perProcessTempfile(localPath)
685
fileWriter, err := os.Create(localPathTmp)
686
if err != nil {
687
return err
688
}
689
defer fileWriter.Close()
690
defer os.RemoveAll(localPathTmp)
691
692
writers := []io.Writer{fileWriter}
693
var digester digest.Digester
694
if expectedDigest != "" {
695
algo := expectedDigest.Algorithm()
696
if !algo.Available() {
697
return fmt.Errorf("unsupported digest algorithm %q", algo)
698
}
699
digester = algo.Digester()
700
hasher := digester.Hash()
701
writers = append(writers, hasher)
702
}
703
multiWriter := io.MultiWriter(writers...)
704
705
if !HideProgress {
706
if description == "" {
707
description = url
708
}
709
// stderr corresponds to the progress bar output
710
fmt.Fprintf(os.Stderr, "Downloading %s\n", description)
711
}
712
bar.Start()
713
if _, err := io.Copy(multiWriter, bar.NewProxyReader(resp.Body)); err != nil {
714
return err
715
}
716
bar.Finish()
717
718
if digester != nil {
719
actualDigest := digester.Digest()
720
if actualDigest != expectedDigest {
721
return fmt.Errorf("expected digest %q, got %q", expectedDigest, actualDigest)
722
}
723
}
724
725
if err := fileWriter.Sync(); err != nil {
726
return err
727
}
728
if err := fileWriter.Close(); err != nil {
729
return err
730
}
731
732
return os.Rename(localPathTmp, localPath)
733
}
734
735
var tempfileCount atomic.Uint64
736
737
// To allow parallel download we use a per-process unique suffix for temporary
738
// files. Renaming the temporary file to the final file is safe without
739
// synchronization on posix.
740
// To make it easy to test we also include a counter ensuring that each
741
// temporary file is unique in the same process.
742
// https://github.com/lima-vm/lima/issues/2722
743
func perProcessTempfile(path string) string {
744
return fmt.Sprintf("%s.tmp.%d.%d", path, os.Getpid(), tempfileCount.Add(1))
745
}
746
747
// CacheEntries returns a map of cache entries.
748
// The key is the SHA256 of the URL.
749
// The value is the path to the cache entry.
750
func CacheEntries(opts ...Opt) (map[string]string, error) {
751
entries := make(map[string]string)
752
var o options
753
if err := o.apply(opts); err != nil {
754
return nil, err
755
}
756
if o.cacheDir == "" {
757
return entries, nil
758
}
759
downloadDir := filepath.Join(o.cacheDir, "download", "by-url-sha256")
760
_, err := os.Stat(downloadDir)
761
if err != nil {
762
if errors.Is(err, os.ErrNotExist) {
763
return entries, nil
764
}
765
return nil, err
766
}
767
cacheEntries, err := os.ReadDir(downloadDir)
768
if err != nil {
769
return nil, err
770
}
771
for _, entry := range cacheEntries {
772
entries[entry.Name()] = filepath.Join(downloadDir, entry.Name())
773
}
774
return entries, nil
775
}
776
777
// CacheKey returns the key for a cache entry of the remote URL.
778
func CacheKey(remote string, decompress bool) string {
779
k := fmt.Sprintf("%x", sha256.Sum256([]byte(remote)))
780
if decompress && decompressor(remote) != "" {
781
k += "+decomp"
782
}
783
return k
784
}
785
786
// RemoveAllCacheDir removes the cache directory.
787
func RemoveAllCacheDir(opts ...Opt) error {
788
var o options
789
if err := o.apply(opts); err != nil {
790
return err
791
}
792
if o.cacheDir == "" {
793
return nil
794
}
795
logrus.Infof("Pruning %q", o.cacheDir)
796
return os.RemoveAll(o.cacheDir)
797
}
798
799