Skip to content

Commit 23850b4

Browse files
authored
Merge pull request #1877 from afbjorklund/start-download-log
Don't download files that are already cached
2 parents d93772a + 4efb676 commit 23850b4

File tree

4 files changed

+148
-17
lines changed

4 files changed

+148
-17
lines changed

pkg/downloader/downloader.go

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ func WithDecompress(decompress bool) Opt {
9797
// - The digest was not specified.
9898
// - The file already exists in the local target path.
9999
//
100-
// When the `data` file exists in the cache dir with `digest.<ALGO>` file,
101-
// the digest is verified by comparing the content of `digest.<ALGO>` with the expected
100+
// When the `data` file exists in the cache dir with `<ALGO>.digest` file,
101+
// the digest is verified by comparing the content of `<ALGO>.digest` with the expected
102102
// digest string. So, the actual digest of the `data` file is not computed.
103103
func WithExpectedDigest(expectedDigest digest.Digest) Opt {
104104
return func(o *options) error {
@@ -183,24 +183,19 @@ func Download(local, remote string, opts ...Opt) (*Result, error) {
183183
return res, nil
184184
}
185185

186-
shad := filepath.Join(o.cacheDir, "download", "by-url-sha256", fmt.Sprintf("%x", sha256.Sum256([]byte(remote))))
186+
shad := cacheDirectoryPath(o.cacheDir, remote)
187187
shadData := filepath.Join(shad, "data")
188-
shadDigest := ""
189-
if o.expectedDigest != "" {
190-
algo := o.expectedDigest.Algorithm().String()
191-
if strings.Contains(algo, "/") || strings.Contains(algo, "\\") {
192-
return nil, fmt.Errorf("invalid digest algorithm %q", algo)
193-
}
194-
shadDigest = filepath.Join(shad, algo+".digest")
188+
shadDigest, err := cacheDigestPath(shad, o.expectedDigest)
189+
if err != nil {
190+
return nil, err
195191
}
196192
if _, err := os.Stat(shadData); err == nil {
197193
logrus.Debugf("file %q is cached as %q", localPath, shadData)
198-
if shadDigestB, err := os.ReadFile(shadDigest); err == nil {
194+
if _, err := os.Stat(shadDigest); err == nil {
199195
logrus.Debugf("Comparing digest %q with the cached digest file %q, not computing the actual digest of %q",
200196
o.expectedDigest, shadDigest, shadData)
201-
shadDigestS := strings.TrimSpace(string(shadDigestB))
202-
if o.expectedDigest.String() != shadDigestS {
203-
return nil, fmt.Errorf("expected digest %q does not match the cached digest %q", o.expectedDigest.String(), shadDigestS)
197+
if err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {
198+
return nil, err
204199
}
205200
if err := copyLocal(localPath, shadData, ext, o.decompress, "", ""); err != nil {
206201
return nil, err
@@ -247,6 +242,73 @@ func Download(local, remote string, opts ...Opt) (*Result, error) {
247242
return res, nil
248243
}
249244

245+
// Cached checks if the remote resource is in the cache.
246+
//
247+
// Download caches the remote resource if WithCache or WithCacheDir option is specified.
248+
// Local files are not cached.
249+
//
250+
// When the cache path already exists, Cached returns Result with StatusUsedCache.
251+
func Cached(remote string, opts ...Opt) (*Result, error) {
252+
var o options
253+
for _, f := range opts {
254+
if err := f(&o); err != nil {
255+
return nil, err
256+
}
257+
}
258+
if o.cacheDir == "" {
259+
return nil, fmt.Errorf("caching-only mode requires the cache directory to be specified")
260+
}
261+
if IsLocal(remote) {
262+
return nil, fmt.Errorf("local files are not cached")
263+
}
264+
265+
shad := cacheDirectoryPath(o.cacheDir, remote)
266+
shadData := filepath.Join(shad, "data")
267+
shadDigest, err := cacheDigestPath(shad, o.expectedDigest)
268+
if err != nil {
269+
return nil, err
270+
}
271+
if _, err := os.Stat(shadData); err != nil {
272+
return nil, err
273+
}
274+
if _, err := os.Stat(shadDigest); err != nil {
275+
if err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {
276+
return nil, err
277+
}
278+
} else {
279+
if err := validateLocalFileDigest(shadData, o.expectedDigest); err != nil {
280+
return nil, err
281+
}
282+
}
283+
res := &Result{
284+
Status: StatusUsedCache,
285+
CachePath: shadData,
286+
ValidatedDigest: o.expectedDigest != "",
287+
}
288+
return res, nil
289+
}
290+
291+
// cacheDirectoryPath returns the cache subdirectory path.
292+
// - "url" file contains the url
293+
// - "data" file contains the data
294+
func cacheDirectoryPath(cacheDir string, remote string) string {
295+
return filepath.Join(cacheDir, "download", "by-url-sha256", fmt.Sprintf("%x", sha256.Sum256([]byte(remote))))
296+
}
297+
298+
// cacheDigestPath returns the cache digest file path.
299+
// - "<ALGO>.digest" contains the digest
300+
func cacheDigestPath(shad string, expectedDigest digest.Digest) (string, error) {
301+
shadDigest := ""
302+
if expectedDigest != "" {
303+
algo := expectedDigest.Algorithm().String()
304+
if strings.Contains(algo, "/") || strings.Contains(algo, "\\") {
305+
return "", fmt.Errorf("invalid digest algorithm %q", algo)
306+
}
307+
shadDigest = filepath.Join(shad, algo+".digest")
308+
}
309+
return shadDigest, nil
310+
}
311+
250312
func IsLocal(s string) bool {
251313
return !strings.Contains(s, "://") || strings.HasPrefix(s, "file://")
252314
}
@@ -278,6 +340,9 @@ func copyLocal(dst, src, ext string, decompress bool, description string, expect
278340
return err
279341
}
280342

343+
if expectedDigest != "" {
344+
logrus.Debugf("verifying digest of local file %q (%s)", srcPath, expectedDigest)
345+
}
281346
if err := validateLocalFileDigest(srcPath, expectedDigest); err != nil {
282347
return err
283348
}
@@ -366,14 +431,28 @@ func decompressLocal(dst, src, ext string, description string) error {
366431
return err
367432
}
368433

434+
func validateCachedDigest(shadDigest string, expectedDigest digest.Digest) error {
435+
if expectedDigest == "" {
436+
return nil
437+
}
438+
shadDigestB, err := os.ReadFile(shadDigest)
439+
if err != nil {
440+
return err
441+
}
442+
shadDigestS := strings.TrimSpace(string(shadDigestB))
443+
if shadDigestS != expectedDigest.String() {
444+
return fmt.Errorf("expected digest %q, got %q", expectedDigest, shadDigestS)
445+
}
446+
return nil
447+
}
448+
369449
func validateLocalFileDigest(localPath string, expectedDigest digest.Digest) error {
370450
if localPath == "" {
371451
return fmt.Errorf("validateLocalFileDigest: got empty localPath")
372452
}
373453
if expectedDigest == "" {
374454
return nil
375455
}
376-
logrus.Debugf("verifying digest of local file %q (%s)", localPath, expectedDigest)
377456
algo := expectedDigest.Algorithm()
378457
if !algo.Available() {
379458
return fmt.Errorf("expected digest algorithm %q is not available", algo)

pkg/downloader/downloader_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os/exec"
66
"path/filepath"
77
"runtime"
8+
"strings"
89
"testing"
910

1011
"github.com/opencontainers/go-digest"
@@ -87,6 +88,24 @@ func TestDownloadRemote(t *testing.T) {
8788
assert.NilError(t, err)
8889
assert.Equal(t, StatusUsedCache, r.Status)
8990
})
91+
t.Run("cached", func(t *testing.T) {
92+
_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
93+
assert.ErrorContains(t, err, "cache directory to be specified")
94+
95+
cacheDir := filepath.Join(t.TempDir(), "cache")
96+
r, err := Download("", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
97+
assert.NilError(t, err)
98+
assert.Equal(t, StatusDownloaded, r.Status)
99+
100+
r, err = Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
101+
assert.NilError(t, err)
102+
assert.Equal(t, StatusUsedCache, r.Status)
103+
assert.Assert(t, strings.HasPrefix(r.CachePath, cacheDir), "expected %s to be in %s", r.CachePath, cacheDir)
104+
105+
wrongDigest := digest.Digest("sha256:8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9")
106+
_, err = Cached(dummyRemoteFileURL, WithExpectedDigest(wrongDigest), WithCacheDir(cacheDir))
107+
assert.ErrorContains(t, err, "expected digest")
108+
})
90109
}
91110

92111
func TestDownloadLocal(t *testing.T) {
@@ -129,6 +148,15 @@ func TestDownloadLocal(t *testing.T) {
129148
os.Remove(localTestFile)
130149
})
131150

151+
t.Run("cached", func(t *testing.T) {
152+
localFile := filepath.Join(t.TempDir(), "test-file")
153+
os.Create(localFile)
154+
testLocalFileURL := "file://" + localFile
155+
156+
cacheDir := filepath.Join(t.TempDir(), "cache")
157+
_, err := Cached(testLocalFileURL, WithCacheDir(cacheDir))
158+
assert.ErrorContains(t, err, "not cached")
159+
})
132160
}
133161

134162
func TestDownloadCompressed(t *testing.T) {

pkg/fileutils/download.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ func DownloadFile(dest string, f limayaml.File, decompress bool, description str
4141
return res.CachePath, nil
4242
}
4343

44+
// CachedFile checks if a file is in the cache, validating the digest if it is available. Returns path in cache.
45+
func CachedFile(f limayaml.File) (string, error) {
46+
res, err := downloader.Cached(f.Location,
47+
downloader.WithCache(),
48+
downloader.WithExpectedDigest(f.Digest))
49+
if err != nil {
50+
return "", fmt.Errorf("cache did not contain %q: %w", f.Location, err)
51+
}
52+
return res.CachePath, nil
53+
}
54+
4455
// Errors compose multiple into a single error.
4556
// Errors filters out ErrSkipped.
4657
func Errors(errs []error) error {

pkg/start/start.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,21 @@ const DefaultWatchHostAgentEventsTimeout = 10 * time.Minute
3434
// ensureNerdctlArchiveCache prefetches the nerdctl-full-VERSION-GOOS-GOARCH.tar.gz archive
3535
// into the cache before launching the hostagent process, so that we can show the progress in tty.
3636
// https://github.com/lima-vm/lima/issues/326
37-
func ensureNerdctlArchiveCache(y *limayaml.LimaYAML) (string, error) {
37+
func ensureNerdctlArchiveCache(y *limayaml.LimaYAML, created bool) (string, error) {
3838
if !*y.Containerd.System && !*y.Containerd.User {
3939
// nerdctl archive is not needed
4040
return "", nil
4141
}
4242

4343
errs := make([]error, len(y.Containerd.Archives))
4444
for i, f := range y.Containerd.Archives {
45+
// Skip downloading again if the file is already in the cache
46+
if created && f.Arch == *y.Arch && !downloader.IsLocal(f.Location) {
47+
path, err := fileutils.CachedFile(f)
48+
if err == nil {
49+
return path, nil
50+
}
51+
}
4552
path, err := fileutils.DownloadFile("", f, false, "the nerdctl archive", *y.Arch)
4653
if err != nil {
4754
errs[i] = err
@@ -80,10 +87,16 @@ func Prepare(_ context.Context, inst *store.Instance) (*Prepared, error) {
8087
return nil, err
8188
}
8289

90+
// Check if the instance has been created (the base disk already exists)
91+
created := false
92+
baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk)
93+
if _, err := os.Stat(baseDisk); err == nil {
94+
created = true
95+
}
8396
if err := limaDriver.CreateDisk(); err != nil {
8497
return nil, err
8598
}
86-
nerdctlArchiveCache, err := ensureNerdctlArchiveCache(y)
99+
nerdctlArchiveCache, err := ensureNerdctlArchiveCache(y, created)
87100
if err != nil {
88101
return nil, err
89102
}

0 commit comments

Comments
 (0)