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