1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
// Package modcache provides a file-based cache for modules.
//
// WARNING: THIS PACKAGE IS EXPERIMENTAL.
// ITS API MAY CHANGE AT ANY TIME.
package modcache
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/rogpeppe/go-internal/lockedfile"
"cuelang.org/go/internal/robustio"
"cuelang.org/go/mod/module"
)
var errNotCached = fmt.Errorf("not in cache")
// readDiskModFile reads a cached go.mod file from disk,
// returning the name of the cache file and the result.
// If the read fails, the caller can use
// writeDiskModFile(file, data) to write a new cache entry.
func (c *Cache) readDiskModFile(mv module.Version) (file string, data []byte, err error) {
return c.readDiskCache(mv, "mod")
}
// writeDiskModFile writes a cue.mod/module.cue cache entry.
// The file name must have been returned by a previous call to readDiskModFile.
func (c *Cache) writeDiskModFile(ctx context.Context, file string, text []byte) error {
return c.writeDiskCache(ctx, file, text)
}
// readDiskCache is the generic "read from a cache file" implementation.
// It takes the revision and an identifying suffix for the kind of data being cached.
// It returns the name of the cache file and the content of the file.
// If the read fails, the caller can use
// writeDiskCache(file, data) to write a new cache entry.
func (c *Cache) readDiskCache(mv module.Version, suffix string) (file string, data []byte, err error) {
file, err = c.cachePath(mv, suffix)
if err != nil {
return "", nil, errNotCached
}
data, err = robustio.ReadFile(file)
if err != nil {
return file, nil, errNotCached
}
return file, data, nil
}
// writeDiskCache is the generic "write to a cache file" implementation.
// The file must have been returned by a previous call to readDiskCache.
func (c *Cache) writeDiskCache(ctx context.Context, file string, data []byte) error {
if file == "" {
return nil
}
// Make sure directory for file exists.
if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
return err
}
// Write the file to a temporary location, and then rename it to its final
// path to reduce the likelihood of a corrupt file existing at that final path.
f, err := tempFile(ctx, filepath.Dir(file), filepath.Base(file), 0666)
if err != nil {
return err
}
defer func() {
// Only call os.Remove on f.Name() if we failed to rename it: otherwise,
// some other process may have created a new file with the same name after
// the rename completed.
if err != nil {
f.Close()
os.Remove(f.Name())
}
}()
if _, err := f.Write(data); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := robustio.Rename(f.Name(), file); err != nil {
return err
}
return nil
}
// downloadDir returns the directory for storing.
// An error will be returned if the module path or version cannot be escaped.
// An error satisfying [errors.Is](err, [fs.ErrNotExist]) will be returned
// along with the directory if the directory does not exist or if the directory
// is not completely populated.
func (c *Cache) downloadDir(m module.Version) (string, error) {
if !m.IsCanonical() {
return "", fmt.Errorf("non-semver module version %q", m.Version())
}
enc, err := module.EscapePath(m.BasePath())
if err != nil {
return "", err
}
encVer, err := module.EscapeVersion(m.Version())
if err != nil {
return "", err
}
// Check whether the directory itself exists.
dir := filepath.Join(c.dir, "extract", enc+"@"+encVer)
if fi, err := os.Stat(dir); os.IsNotExist(err) {
return dir, err
} else if err != nil {
return dir, &downloadDirPartialError{dir, err}
} else if !fi.IsDir() {
return dir, &downloadDirPartialError{dir, errors.New("not a directory")}
}
// Check if a .partial file exists. This is created at the beginning of
// a download and removed after the zip is extracted.
partialPath, err := c.cachePath(m, "partial")
if err != nil {
return dir, err
}
if _, err := os.Stat(partialPath); err == nil {
return dir, &downloadDirPartialError{dir, errors.New("not completely extracted")}
} else if !os.IsNotExist(err) {
return dir, err
}
return dir, nil
}
func (c *Cache) cachePath(m module.Version, suffix string) (string, error) {
if !m.IsValid() || m.Version() == "" {
return "", fmt.Errorf("non-semver module version %q", m)
}
esc, err := module.EscapePath(m.BasePath())
if err != nil {
return "", err
}
encVer, err := module.EscapeVersion(m.Version())
if err != nil {
return "", err
}
return filepath.Join(c.dir, "download", esc, "/@v", encVer+"."+suffix), nil
}
// downloadDirPartialError is returned by DownloadDir if a module directory
// exists but was not completely populated.
//
// downloadDirPartialError is equivalent to fs.ErrNotExist.
type downloadDirPartialError struct {
Dir string
Err error
}
func (e *downloadDirPartialError) Error() string { return fmt.Sprintf("%s: %v", e.Dir, e.Err) }
func (e *downloadDirPartialError) Is(err error) bool { return err == fs.ErrNotExist }
// lockVersion locks a file within the module cache that guards the downloading
// and extraction of module data for the given module version.
func (c *Cache) lockVersion(mod module.Version) (unlock func(), err error) {
path, err := c.cachePath(mod, "lock")
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return nil, err
}
return lockedfile.MutexAt(path).Lock()
}
|