Skip to content

Commit 0305550

Browse files
committed
add HTTP cache (~/Library/Caches/lima)
The `~/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>` directory contains the following files: - `url`: raw url text, without "\n" - `data`: data Signed-off-by: Akihiro Suda <[email protected]>
1 parent 64d4b1e commit 0305550

File tree

7 files changed

+241
-22
lines changed

7 files changed

+241
-22
lines changed

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func newApp() *cli.App {
4646
shellCommand,
4747
lsCommand,
4848
deleteCommand,
49+
pruneCommand,
4950
completionCommand,
5051
}
5152
return app

cmd/limactl/prune.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/sirupsen/logrus"
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
var pruneCommand = &cli.Command{
12+
Name: "prune",
13+
Usage: "Prune garbage objects",
14+
Action: pruneAction,
15+
}
16+
17+
func pruneAction(clicontext *cli.Context) error {
18+
ucd, err := os.UserCacheDir()
19+
if err != nil {
20+
return err
21+
}
22+
cacheDir := filepath.Join(ucd, "lima")
23+
logrus.Infof("Pruning %q", cacheDir)
24+
return os.RemoveAll(cacheDir)
25+
}

docs/internal.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ An instance directory contains the following files:
1313
- `ga.sock`: Forwarded to `/run/user/$UID/lima-guestagent.sock`
1414
- `serial.log`: QEMU serial log, for debugging
1515
- `serial.sock`: QEMU serial socket, for debugging (Usage: `socat -,echo=0,icanon=0 unix-connect:serial.sock`)
16+
17+
18+
## Cache directory (`~/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>`)
19+
20+
The directory contains the following files:
21+
22+
- `url`: raw url text, without "\n"
23+
- `data`: data

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/AkihiroSuda/sshocker v0.1.1-0.20210510144941-56aa3c7472b0
77
github.com/alessio/shellescape v1.4.1
88
github.com/containerd/containerd v1.5.0
9+
github.com/containerd/continuity v0.1.0
910
github.com/diskfs/go-diskfs v1.1.2-0.20210512141858-8a6b8b88d14a
1011
github.com/docker/go-units v0.4.0
1112
github.com/gorilla/mux v1.8.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL
164164
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
165165
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
166166
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
167+
github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8=
167168
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
168169
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
169170
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
@@ -524,6 +525,7 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1
524525
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
525526
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
526527
github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
528+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
527529
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
528530
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
529531
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
@@ -812,6 +814,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
812814
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
813815
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
814816
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
817+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
815818
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
816819
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
817820
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

pkg/downloader/downloader.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package downloader
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/AkihiroSuda/lima/pkg/localpathutil"
12+
"github.com/containerd/continuity/fs"
13+
"github.com/pkg/errors"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
type Status = string
18+
19+
const (
20+
StatusUnknown Status = ""
21+
StatusDownloaded Status = "downloaded"
22+
StatusSkipped Status = "skipped"
23+
StatusUsedCache Status = "used-cache"
24+
)
25+
26+
type Result struct {
27+
Status Status
28+
CachePath string // "/Users/foo/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>/data"
29+
}
30+
31+
type options struct {
32+
cacheDir string // default: empty (disables caching)
33+
}
34+
35+
type Opt func(*options) error
36+
37+
// WithCache enables caching using filepath.Join(os.UserCacheDir(), "lima") as the cache dir.
38+
func WithCache() Opt {
39+
return func(o *options) error {
40+
ucd, err := os.UserCacheDir()
41+
if err != nil {
42+
return err
43+
}
44+
cacheDir := filepath.Join(ucd, "lima")
45+
return WithCacheDir(cacheDir)(o)
46+
}
47+
}
48+
49+
// WithCacheDir enables caching using the specified dir.
50+
// Empty value disables caching.
51+
func WithCacheDir(cacheDir string) Opt {
52+
return func(o *options) error {
53+
o.cacheDir = cacheDir
54+
return nil
55+
}
56+
}
57+
58+
func Download(local, remote string, opts ...Opt) (*Result, error) {
59+
var o options
60+
for _, f := range opts {
61+
if err := f(&o); err != nil {
62+
return nil, err
63+
}
64+
}
65+
localPath, err := localPath(local)
66+
if err != nil {
67+
return nil, err
68+
}
69+
if _, err := os.Stat(localPath); err == nil {
70+
logrus.Debugf("file %q already exists, skipping downloading from %q", localPath, remote)
71+
res := &Result{
72+
Status: StatusSkipped,
73+
}
74+
return res, nil
75+
} else if !errors.Is(err, os.ErrNotExist) {
76+
return nil, err
77+
}
78+
79+
localPathDir := filepath.Dir(localPath)
80+
if err := os.MkdirAll(localPathDir, 0755); err != nil {
81+
return nil, err
82+
}
83+
84+
if isLocal(remote) {
85+
if err := copyLocal(localPath, remote); err != nil {
86+
return nil, err
87+
}
88+
res := &Result{
89+
Status: StatusDownloaded,
90+
}
91+
return res, nil
92+
}
93+
94+
if o.cacheDir == "" {
95+
if err := downloadHTTP(localPath, remote); err != nil {
96+
return nil, err
97+
}
98+
res := &Result{
99+
Status: StatusDownloaded,
100+
}
101+
return res, nil
102+
}
103+
104+
shad := filepath.Join(o.cacheDir, "download", "by-url-sha256", fmt.Sprintf("%x", sha256.Sum256([]byte(remote))))
105+
shadData := filepath.Join(shad, "data")
106+
if _, err := os.Stat(shadData); err == nil {
107+
logrus.Debugf("file %q is cached as %q", localPath, shadData)
108+
if err := copyLocal(localPath, shadData); err != nil {
109+
return nil, err
110+
}
111+
res := &Result{
112+
Status: StatusUsedCache,
113+
CachePath: shadData,
114+
}
115+
return res, nil
116+
}
117+
if err := os.RemoveAll(shad); err != nil {
118+
return nil, err
119+
}
120+
if err := os.MkdirAll(shad, 0700); err != nil {
121+
return nil, err
122+
}
123+
shadURL := filepath.Join(shad, "url")
124+
if err := os.WriteFile(shadURL, []byte(remote), 0644); err != nil {
125+
return nil, err
126+
}
127+
if err := downloadHTTP(shadData, remote); err != nil {
128+
return nil, err
129+
}
130+
if err := copyLocal(localPath, shadData); err != nil {
131+
return nil, err
132+
}
133+
134+
res := &Result{
135+
Status: StatusDownloaded,
136+
CachePath: shadData,
137+
}
138+
return res, nil
139+
}
140+
141+
func isLocal(s string) bool {
142+
return !strings.Contains(s, "://") || strings.HasPrefix(s, "file://")
143+
}
144+
145+
func localPath(s string) (string, error) {
146+
if !isLocal(s) {
147+
return "", errors.Errorf("got non-local path: %q", s)
148+
}
149+
if strings.HasPrefix(s, "file://") {
150+
res := strings.TrimPrefix(s, "file://")
151+
if !filepath.IsAbs(res) {
152+
return "", errors.Errorf("got non-absolute path %q", res)
153+
}
154+
return res, nil
155+
}
156+
return localpathutil.Expand(s)
157+
}
158+
159+
func copyLocal(dst, src string) error {
160+
srcPath, err := localPath(src)
161+
if err != nil {
162+
return err
163+
}
164+
dstPath, err := localPath(dst)
165+
if err != nil {
166+
return err
167+
}
168+
return fs.CopyFile(dstPath, srcPath)
169+
}
170+
171+
func downloadHTTP(localPath, url string) error {
172+
logrus.Debugf("downloading %q into %q", url, localPath)
173+
localPathTmp := localPath + ".tmp"
174+
if err := os.RemoveAll(localPathTmp); err != nil {
175+
return err
176+
}
177+
// use curl for printing progress
178+
cmd := exec.Command("curl", "-fSL", "-o", localPathTmp, url)
179+
cmd.Stdout = os.Stdout
180+
cmd.Stderr = os.Stderr
181+
if err := cmd.Run(); err != nil {
182+
return errors.Wrapf(err, "failed to run %v", cmd.Args)
183+
}
184+
if err := os.RemoveAll(localPath); err != nil {
185+
return err
186+
}
187+
if err := os.Rename(localPathTmp, localPath); err != nil {
188+
return err
189+
}
190+
return nil
191+
}

pkg/qemu/qemu.go

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import (
77
"path/filepath"
88
"runtime"
99
"strconv"
10-
"strings"
1110

11+
"github.com/AkihiroSuda/lima/pkg/downloader"
1212
"github.com/AkihiroSuda/lima/pkg/limayaml"
13-
"github.com/AkihiroSuda/lima/pkg/localpathutil"
1413
"github.com/docker/go-units"
1514
"github.com/pkg/errors"
1615
"github.com/sirupsen/logrus"
@@ -31,35 +30,26 @@ func EnsureDisk(cfg Config) error {
3130

3231
baseDisk := filepath.Join(cfg.InstanceDir, "basedisk")
3332
if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) {
34-
baseDiskTmp := filepath.Join(cfg.InstanceDir, "basedisk.tmp")
35-
if err := os.RemoveAll(baseDiskTmp); err != nil {
36-
return err
37-
}
3833
var ensuredBaseDisk bool
3934
errs := make([]error, len(cfg.LimaYAML.Images))
4035
for i, f := range cfg.LimaYAML.Images {
4136
if f.Arch != cfg.LimaYAML.Arch {
4237
errs[i] = fmt.Errorf("unsupported arch: %q", f.Arch)
4338
continue
4439
}
45-
url := f.Location
46-
if !strings.Contains(url, "://") {
47-
expanded, err := localpathutil.Expand(url)
48-
if err != nil {
49-
return err
50-
}
51-
url = "file://" + expanded
52-
}
53-
cmd := exec.Command("curl", "-fSL", "-o", baseDiskTmp, url)
54-
cmd.Stdout = os.Stdout
55-
cmd.Stderr = os.Stderr
56-
logrus.Infof("Attempting to download the image from %q", url)
57-
if err := cmd.Run(); err != nil {
58-
errs[i] = errors.Wrapf(err, "failed to run %v", cmd.Args)
40+
logrus.Infof("Attempting to download the image from %q", f.Location)
41+
res, err := downloader.Download(baseDisk, f.Location, downloader.WithCache())
42+
if err != nil {
43+
errs[i] = errors.Wrapf(err, "failed to download %q", f.Location)
5944
continue
6045
}
61-
if err := os.Rename(baseDiskTmp, baseDisk); err != nil {
62-
return err
46+
switch res.Status {
47+
case downloader.StatusDownloaded:
48+
logrus.Infof("Downloaded image from %q", f.Location)
49+
case downloader.StatusUsedCache:
50+
logrus.Infof("Using cache %q", res.CachePath)
51+
default:
52+
logrus.Warnf("Unexpected result from downloader.Download(): %+v", res)
6353
}
6454
ensuredBaseDisk = true
6555
break

0 commit comments

Comments
 (0)