this repo has no description
at master 381 lines 11 kB view raw
1package modcache 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "io/fs" 9 "log" 10 "math/rand/v2" 11 "os" 12 "path/filepath" 13 "slices" 14 "strconv" 15 "strings" 16 17 "cuelang.org/go/internal/par" 18 "cuelang.org/go/internal/robustio" 19 "cuelang.org/go/mod/modfile" 20 "cuelang.org/go/mod/modregistry" 21 "cuelang.org/go/mod/module" 22 "cuelang.org/go/mod/modzip" 23) 24 25const logging = false // TODO hook this up to CUE_DEBUG 26 27// New returns r wrapped inside a caching layer that 28// stores persistent cached content inside the given 29// OS directory, typically ${CUE_CACHE_DIR}. 30// 31// The `module.SourceLoc.FS` fields in the locations 32// returned by the registry implement the `OSRootFS` interface, 33// allowing a caller to find the native OS filepath where modules 34// are stored. 35// 36// The returned type implements [modconfig.Registry] 37// and [modconfig.CachedRegistry]. 38func New(registry *modregistry.Client, dir string) (*Cache, error) { 39 info, err := os.Stat(dir) 40 if err == nil && !info.IsDir() { 41 return nil, fmt.Errorf("%q is not a directory", dir) 42 } 43 return &Cache{ 44 dir: filepath.Join(dir, "mod"), 45 reg: registry, 46 }, nil 47} 48 49type Cache struct { 50 dir string // typically ${CUE_CACHE_DIR}/mod 51 reg *modregistry.Client 52 downloadZipCache par.ErrCache[module.Version, string] 53 modFileCache par.ErrCache[string, []byte] 54} 55 56func (c *Cache) Requirements(ctx context.Context, mv module.Version) ([]module.Version, error) { 57 data, err := c.downloadModFile(ctx, mv) 58 if err != nil { 59 return nil, err 60 } 61 mf, err := modfile.Parse(data, mv.String()) 62 if err != nil { 63 return nil, fmt.Errorf("cannot parse module file from %v: %v", mv, err) 64 } 65 return mf.DepVersions(), nil 66} 67 68// FetchFromCache implements [cuelang.org/go/mod/modconfig.CachedRegistry]. 69func (c *Cache) FetchFromCache(mv module.Version) (module.SourceLoc, error) { 70 dir, err := c.downloadDir(mv) 71 if err != nil { 72 if errors.Is(err, fs.ErrNotExist) { 73 return module.SourceLoc{}, modregistry.ErrNotFound 74 } 75 return module.SourceLoc{}, err 76 } 77 return c.dirToLocation(dir), nil 78} 79 80// Fetch returns the location of the contents for the given module 81// version, downloading it if necessary. 82func (c *Cache) Fetch(ctx context.Context, mv module.Version) (module.SourceLoc, error) { 83 dir, err := c.downloadDir(mv) 84 if err == nil { 85 // The directory has already been completely extracted (no .partial file exists). 86 return c.dirToLocation(dir), nil 87 } 88 if dir == "" || !errors.Is(err, fs.ErrNotExist) { 89 return module.SourceLoc{}, err 90 } 91 92 // To avoid cluttering the cache with extraneous files, 93 // DownloadZip uses the same lockfile as Download. 94 // Invoke DownloadZip before locking the file. 95 zipfile, err := c.downloadZip(ctx, mv) 96 if err != nil { 97 return module.SourceLoc{}, err 98 } 99 100 unlock, err := c.lockVersion(mv) 101 if err != nil { 102 return module.SourceLoc{}, err 103 } 104 defer unlock() 105 106 // Check whether the directory was populated while we were waiting on the lock. 107 _, dirErr := c.downloadDir(mv) 108 if dirErr == nil { 109 return c.dirToLocation(dir), nil 110 } 111 _, dirExists := dirErr.(*downloadDirPartialError) 112 113 // Clean up any partially extracted directories (indicated by 114 // DownloadDirPartialError, usually because of a .partial file). This is only 115 // safe to do because the lock file ensures that their writers are no longer 116 // active. 117 parentDir := filepath.Dir(dir) 118 tmpPrefix := filepath.Base(dir) + ".tmp-" 119 120 entries, _ := os.ReadDir(parentDir) 121 for _, entry := range entries { 122 if strings.HasPrefix(entry.Name(), tmpPrefix) { 123 RemoveAll(filepath.Join(parentDir, entry.Name())) // best effort 124 } 125 } 126 if dirExists { 127 if err := RemoveAll(dir); err != nil { 128 return module.SourceLoc{}, err 129 } 130 } 131 132 partialPath, err := c.cachePath(mv, "partial") 133 if err != nil { 134 return module.SourceLoc{}, err 135 } 136 137 // Extract the module zip directory at its final location. 138 // 139 // To prevent other processes from reading the directory if we crash, 140 // create a .partial file before extracting the directory, and delete 141 // the .partial file afterward (all while holding the lock). 142 // 143 // A technique used previously was to extract to a temporary directory with a random name 144 // then rename it into place with os.Rename. On Windows, this can fail with 145 // ERROR_ACCESS_DENIED when another process (usually an anti-virus scanner) 146 // opened files in the temporary directory. 147 if err := os.MkdirAll(parentDir, 0777); err != nil { 148 return module.SourceLoc{}, err 149 } 150 if err := os.WriteFile(partialPath, nil, 0666); err != nil { 151 return module.SourceLoc{}, err 152 } 153 if err := modzip.Unzip(dir, mv, zipfile); err != nil { 154 if rmErr := RemoveAll(dir); rmErr == nil { 155 os.Remove(partialPath) 156 } 157 return module.SourceLoc{}, err 158 } 159 if err := os.Remove(partialPath); err != nil { 160 return module.SourceLoc{}, err 161 } 162 makeDirsReadOnly(dir) 163 return c.dirToLocation(dir), nil 164} 165 166// ModuleVersions implements [modload.Registry.ModuleVersions]. 167func (c *Cache) ModuleVersions(ctx context.Context, mpath string) ([]string, error) { 168 // TODO should this do any kind of short-term caching? 169 return c.reg.ModuleVersions(ctx, mpath) 170} 171 172func (c *Cache) downloadZip(ctx context.Context, mv module.Version) (zipfile string, err error) { 173 return c.downloadZipCache.Do(mv, func() (string, error) { 174 zipfile, err := c.cachePath(mv, "zip") 175 if err != nil { 176 return "", err 177 } 178 179 // Return without locking if the zip file exists. 180 if _, err := os.Stat(zipfile); err == nil { 181 return zipfile, nil 182 } 183 logf("cue: downloading %s", mv) 184 unlock, err := c.lockVersion(mv) 185 if err != nil { 186 return "", err 187 } 188 defer unlock() 189 190 if err := c.downloadZip1(ctx, mv, zipfile); err != nil { 191 return "", err 192 } 193 return zipfile, nil 194 }) 195} 196 197func (c *Cache) downloadZip1(ctx context.Context, mod module.Version, zipfile string) (err error) { 198 // Double-check that the zipfile was not created while we were waiting for 199 // the lock in downloadZip. 200 if _, err := os.Stat(zipfile); err == nil { 201 return nil 202 } 203 204 // Create parent directories. 205 if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil { 206 return err 207 } 208 209 // Clean up any remaining tempfiles from previous runs. 210 // This is only safe to do because the lock file ensures that their 211 // writers are no longer active. 212 tmpPattern := filepath.Base(zipfile) + "*.tmp" 213 if old, err := filepath.Glob(filepath.Join(quoteGlob(filepath.Dir(zipfile)), tmpPattern)); err == nil { 214 for _, path := range old { 215 os.Remove(path) // best effort 216 } 217 } 218 219 // From here to the os.Rename call below is functionally almost equivalent to 220 // renameio.WriteToFile. We avoid using that so that we have control over the 221 // names of the temporary files (see the cleanup above) and to avoid adding 222 // renameio as an extra dependency. 223 f, err := tempFile(ctx, filepath.Dir(zipfile), filepath.Base(zipfile), 0666) 224 if err != nil { 225 return err 226 } 227 defer func() { 228 if err != nil { 229 f.Close() 230 os.Remove(f.Name()) 231 } 232 }() 233 234 // TODO cache the result of GetModule so we don't have to do 235 // an extra round trip when we've already fetched the module file. 236 m, err := c.reg.GetModule(ctx, mod) 237 if err != nil { 238 return err 239 } 240 r, err := m.GetZip(ctx) 241 if err != nil { 242 return err 243 } 244 defer r.Close() 245 if _, err := io.Copy(f, r); err != nil { 246 return fmt.Errorf("failed to get module zip contents: %v", err) 247 } 248 if err := f.Close(); err != nil { 249 return err 250 } 251 if err := os.Rename(f.Name(), zipfile); err != nil { 252 return err 253 } 254 // TODO should we check the zip file for well-formedness? 255 // TODO: Should we make the .zip file read-only to discourage tampering? 256 return nil 257} 258 259func (c *Cache) downloadModFile(ctx context.Context, mod module.Version) ([]byte, error) { 260 return c.modFileCache.Do(mod.String(), func() ([]byte, error) { 261 modfile, data, err := c.readDiskModFile(mod) 262 if err == nil { 263 return data, nil 264 } 265 logf("cue: downloading %s", mod) 266 unlock, err := c.lockVersion(mod) 267 if err != nil { 268 return nil, err 269 } 270 defer unlock() 271 // Double-check that the file hasn't been created while we were 272 // acquiring the lock. 273 _, data, err = c.readDiskModFile(mod) 274 if err == nil { 275 return data, nil 276 } 277 return c.downloadModFile1(ctx, mod, modfile) 278 }) 279} 280 281func (c *Cache) downloadModFile1(ctx context.Context, mod module.Version, modfile string) ([]byte, error) { 282 m, err := c.reg.GetModule(ctx, mod) 283 if err != nil { 284 return nil, err 285 } 286 data, err := m.ModuleFile(ctx) 287 if err != nil { 288 return nil, err 289 } 290 if err := c.writeDiskModFile(ctx, modfile, data); err != nil { 291 return nil, err 292 } 293 return data, nil 294} 295 296func (c *Cache) dirToLocation(fpath string) module.SourceLoc { 297 return module.SourceLoc{ 298 FS: module.OSDirFS(fpath), 299 Dir: ".", 300 } 301} 302 303// makeDirsReadOnly makes a best-effort attempt to remove write permissions for dir 304// and its transitive contents. 305func makeDirsReadOnly(dir string) { 306 type pathMode struct { 307 path string 308 mode fs.FileMode 309 } 310 var dirs []pathMode // in lexical order 311 filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 312 if err == nil && d.IsDir() { 313 info, err := d.Info() 314 if err == nil && info.Mode()&0222 != 0 { 315 dirs = append(dirs, pathMode{path, info.Mode()}) 316 } 317 } 318 return nil 319 }) 320 321 // Run over list backward to chmod children before parents. 322 for _, dir := range slices.Backward(dirs) { 323 os.Chmod(dir.path, dir.mode&^0222) 324 } 325} 326 327// RemoveAll removes a directory written by the cache, first applying 328// any permission changes needed to do so. 329func RemoveAll(dir string) error { 330 // Module cache has 0555 directories; make them writable in order to remove content. 331 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error { 332 if err != nil { 333 return nil // ignore errors walking in file system 334 } 335 if info.IsDir() { 336 os.Chmod(path, 0777) 337 } 338 return nil 339 }) 340 return robustio.RemoveAll(dir) 341} 342 343// quoteGlob returns s with all Glob metacharacters quoted. 344// We don't try to handle backslash here, as that can appear in a 345// file path on Windows. 346func quoteGlob(s string) string { 347 if !strings.ContainsAny(s, `*?[]`) { 348 return s 349 } 350 var sb strings.Builder 351 for _, c := range s { 352 switch c { 353 case '*', '?', '[', ']': 354 sb.WriteByte('\\') 355 } 356 sb.WriteRune(c) 357 } 358 return sb.String() 359} 360 361// tempFile creates a new temporary file with given permission bits. 362func tempFile(ctx context.Context, dir, prefix string, perm fs.FileMode) (f *os.File, err error) { 363 for range 10000 { 364 name := filepath.Join(dir, prefix+strconv.Itoa(rand.IntN(1000000000))+".tmp") 365 f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm) 366 if os.IsExist(err) { 367 if ctx.Err() != nil { 368 return nil, ctx.Err() 369 } 370 continue 371 } 372 break 373 } 374 return 375} 376 377func logf(f string, a ...any) { 378 if logging { 379 log.Printf(f, a...) 380 } 381}