1 package modcache
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "log"
10 "math/rand"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15
16 "github.com/rogpeppe/go-internal/robustio"
17
18 "cuelang.org/go/internal/mod/modload"
19 "cuelang.org/go/internal/par"
20 "cuelang.org/go/mod/modfile"
21 "cuelang.org/go/mod/modregistry"
22 "cuelang.org/go/mod/module"
23 "cuelang.org/go/mod/modzip"
24 )
25
26 const logging = false
27
28
29
30
31
32
33
34
35
36 func New(registry *modregistry.Client, dir string) (modload.Registry, error) {
37 info, err := os.Stat(dir)
38 if err == nil && !info.IsDir() {
39 return nil, fmt.Errorf("%q is not a directory", dir)
40 }
41 return &cache{
42 dir: dir,
43 reg: registry,
44 }, nil
45 }
46
47 type cache struct {
48 dir string
49 reg *modregistry.Client
50 downloadZipCache par.ErrCache[module.Version, string]
51 modFileCache par.ErrCache[string, []byte]
52 }
53
54 func (c *cache) Requirements(ctx context.Context, mv module.Version) ([]module.Version, error) {
55 data, err := c.downloadModFile(ctx, mv)
56 if err != nil {
57 return nil, err
58 }
59 mf, err := modfile.Parse(data, mv.String())
60 if err != nil {
61 return nil, fmt.Errorf("cannot parse module file from %v: %v", mv, err)
62 }
63 return mf.DepVersions(), nil
64 }
65
66
67
68 func (c *cache) Fetch(ctx context.Context, mv module.Version) (module.SourceLoc, error) {
69 dir, err := c.downloadDir(ctx, mv)
70 if err == nil {
71
72 return c.dirToLocation(dir), nil
73 }
74 if dir == "" || !errors.Is(err, fs.ErrNotExist) {
75 return module.SourceLoc{}, err
76 }
77
78
79
80
81 zipfile, err := c.downloadZip(ctx, mv)
82 if err != nil {
83 return module.SourceLoc{}, err
84 }
85
86 unlock, err := c.lockVersion(ctx, mv)
87 if err != nil {
88 return module.SourceLoc{}, err
89 }
90 defer unlock()
91
92
93 _, dirErr := c.downloadDir(ctx, mv)
94 if dirErr == nil {
95 return c.dirToLocation(dir), nil
96 }
97 _, dirExists := dirErr.(*downloadDirPartialError)
98
99
100
101
102
103 parentDir := filepath.Dir(dir)
104 tmpPrefix := filepath.Base(dir) + ".tmp-"
105
106 entries, _ := os.ReadDir(parentDir)
107 for _, entry := range entries {
108 if strings.HasPrefix(entry.Name(), tmpPrefix) {
109 RemoveAll(filepath.Join(parentDir, entry.Name()))
110 }
111 }
112 if dirExists {
113 if err := RemoveAll(dir); err != nil {
114 return module.SourceLoc{}, err
115 }
116 }
117
118 partialPath, err := c.cachePath(ctx, mv, "partial")
119 if err != nil {
120 return module.SourceLoc{}, err
121 }
122
123
124
125
126
127
128
129
130
131
132
133 if err := os.MkdirAll(parentDir, 0777); err != nil {
134 return module.SourceLoc{}, err
135 }
136 if err := os.WriteFile(partialPath, nil, 0666); err != nil {
137 return module.SourceLoc{}, err
138 }
139 if err := modzip.Unzip(dir, mv, zipfile); err != nil {
140 if rmErr := RemoveAll(dir); rmErr == nil {
141 os.Remove(partialPath)
142 }
143 return module.SourceLoc{}, err
144 }
145 if err := os.Remove(partialPath); err != nil {
146 return module.SourceLoc{}, err
147 }
148 makeDirsReadOnly(dir)
149 return c.dirToLocation(dir), nil
150 }
151
152
153 func (c *cache) ModuleVersions(ctx context.Context, mpath string) ([]string, error) {
154
155 return c.reg.ModuleVersions(ctx, mpath)
156 }
157
158 func (c *cache) downloadZip(ctx context.Context, mv module.Version) (zipfile string, err error) {
159 return c.downloadZipCache.Do(mv, func() (string, error) {
160 zipfile, err := c.cachePath(ctx, mv, "zip")
161 if err != nil {
162 return "", err
163 }
164
165
166 if _, err := os.Stat(zipfile); err == nil {
167 return zipfile, nil
168 }
169 logf("cue: downloading %s", mv)
170 unlock, err := c.lockVersion(ctx, mv)
171 if err != nil {
172 return "", err
173 }
174 defer unlock()
175
176 if err := c.downloadZip1(ctx, mv, zipfile); err != nil {
177 return "", err
178 }
179 return zipfile, nil
180 })
181 }
182
183 func (c *cache) downloadZip1(ctx context.Context, mod module.Version, zipfile string) (err error) {
184
185
186 if _, err := os.Stat(zipfile); err == nil {
187 return nil
188 }
189
190
191 if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil {
192 return err
193 }
194
195
196
197
198 tmpPattern := filepath.Base(zipfile) + "*.tmp"
199 if old, err := filepath.Glob(filepath.Join(quoteGlob(filepath.Dir(zipfile)), tmpPattern)); err == nil {
200 for _, path := range old {
201 os.Remove(path)
202 }
203 }
204
205
206
207
208
209 f, err := tempFile(ctx, filepath.Dir(zipfile), filepath.Base(zipfile), 0666)
210 if err != nil {
211 return err
212 }
213 defer func() {
214 if err != nil {
215 f.Close()
216 os.Remove(f.Name())
217 }
218 }()
219
220
221
222 m, err := c.reg.GetModule(ctx, mod)
223 if err != nil {
224 return err
225 }
226 r, err := m.GetZip(ctx)
227 if err != nil {
228 return err
229 }
230 defer r.Close()
231 if _, err := io.Copy(f, r); err != nil {
232 return fmt.Errorf("failed to get module zip contents: %v", err)
233 }
234 if err := f.Close(); err != nil {
235 return err
236 }
237 if err := os.Rename(f.Name(), zipfile); err != nil {
238 return err
239 }
240
241
242 return nil
243 }
244
245 func (c *cache) downloadModFile(ctx context.Context, mod module.Version) ([]byte, error) {
246 return c.modFileCache.Do(mod.String(), func() ([]byte, error) {
247 modfile, data, err := c.readDiskModFile(ctx, mod)
248 if err == nil {
249 return data, nil
250 }
251 logf("cue: downloading %s", mod)
252 unlock, err := c.lockVersion(ctx, mod)
253 if err != nil {
254 return nil, err
255 }
256 defer unlock()
257
258
259 _, data, err = c.readDiskModFile(ctx, mod)
260 if err == nil {
261 return data, nil
262 }
263 return c.downloadModFile1(ctx, mod, modfile)
264 })
265 }
266
267 func (c *cache) downloadModFile1(ctx context.Context, mod module.Version, modfile string) ([]byte, error) {
268 m, err := c.reg.GetModule(ctx, mod)
269 if err != nil {
270 return nil, err
271 }
272 data, err := m.ModuleFile(ctx)
273 if err != nil {
274 return nil, err
275 }
276 if err := c.writeDiskModFile(ctx, modfile, data); err != nil {
277 return nil, err
278 }
279 return data, nil
280 }
281
282 func (c *cache) dirToLocation(fpath string) module.SourceLoc {
283 return module.SourceLoc{
284 FS: module.OSDirFS(fpath),
285 Dir: ".",
286 }
287 }
288
289
290
291 func makeDirsReadOnly(dir string) {
292 type pathMode struct {
293 path string
294 mode fs.FileMode
295 }
296 var dirs []pathMode
297 filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
298 if err == nil && d.IsDir() {
299 info, err := d.Info()
300 if err == nil && info.Mode()&0222 != 0 {
301 dirs = append(dirs, pathMode{path, info.Mode()})
302 }
303 }
304 return nil
305 })
306
307
308 for i := len(dirs) - 1; i >= 0; i-- {
309 os.Chmod(dirs[i].path, dirs[i].mode&^0222)
310 }
311 }
312
313
314
315 func RemoveAll(dir string) error {
316
317 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
318 if err != nil {
319 return nil
320 }
321 if info.IsDir() {
322 os.Chmod(path, 0777)
323 }
324 return nil
325 })
326 return robustio.RemoveAll(dir)
327 }
328
329
330
331
332 func quoteGlob(s string) string {
333 if !strings.ContainsAny(s, `*?[]`) {
334 return s
335 }
336 var sb strings.Builder
337 for _, c := range s {
338 switch c {
339 case '*', '?', '[', ']':
340 sb.WriteByte('\\')
341 }
342 sb.WriteRune(c)
343 }
344 return sb.String()
345 }
346
347
348 func tempFile(ctx context.Context, dir, prefix string, perm fs.FileMode) (f *os.File, err error) {
349 for i := 0; i < 10000; i++ {
350 name := filepath.Join(dir, prefix+strconv.Itoa(rand.Intn(1000000000))+".tmp")
351 f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
352 if os.IsExist(err) {
353 if ctx.Err() != nil {
354 return nil, ctx.Err()
355 }
356 continue
357 }
358 break
359 }
360 return
361 }
362
363 func logf(f string, a ...any) {
364 if logging {
365 log.Printf(f, a...)
366 }
367 }
368
View as plain text