github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/artifactcache/artifactcache.go (about) 1 package artifactcache 2 3 import ( 4 "encoding/json" 5 "os" 6 "path/filepath" 7 "strconv" 8 "sync" 9 "time" 10 11 "github.com/ActiveState/cli/internal/constants" 12 "github.com/ActiveState/cli/internal/errs" 13 "github.com/ActiveState/cli/internal/fileutils" 14 "github.com/ActiveState/cli/internal/installation/storage" 15 "github.com/ActiveState/cli/internal/logging" 16 "github.com/ActiveState/cli/internal/multilog" 17 "github.com/ActiveState/cli/internal/rollbar" 18 "github.com/go-openapi/strfmt" 19 ) 20 21 type cachedArtifact struct { 22 Id strfmt.UUID `json:"id"` 23 ArchivePath string `json:"archivePath"` 24 Size int64 `json:"size"` 25 LastAccessTime int64 `json:"lastAccessTime"` 26 } 27 28 // ArtifactCache is a cache of downloaded artifacts from the ActiveState Platform. 29 // The State Tool prefers to use this cache instead of redownloading artifacts. 30 type ArtifactCache struct { 31 dir string 32 infoJson string 33 maxSize int64 // bytes 34 currentSize int64 // bytes 35 artifacts map[strfmt.UUID]*cachedArtifact 36 mutex sync.Mutex 37 timeSpentCopying time.Duration 38 sizeCopied int64 // bytes 39 } 40 41 const MB int64 = 1024 * 1024 42 43 // New returns a new artifact cache in the State Tool's cache directory with the default maximum size of 1GB. 44 func New() (*ArtifactCache, error) { 45 var maxSize int64 = 1024 * MB 46 // TODO: size should be configurable and the user should be warned of an invalid size. 47 // https://activestatef.atlassian.net/browse/DX-984 48 if sizeOverride, err := strconv.Atoi(os.Getenv(constants.ArtifactCacheSizeEnvVarName)); err != nil && sizeOverride > 0 { 49 maxSize = int64(sizeOverride) * MB 50 } 51 return newWithDirAndSize(storage.ArtifactCacheDir(), maxSize) 52 } 53 54 func newWithDirAndSize(dir string, maxSize int64) (*ArtifactCache, error) { 55 err := fileutils.MkdirUnlessExists(dir) 56 if err != nil { 57 return nil, errs.Wrap(err, "Could not create artifact cache directory '%s'", dir) 58 } 59 60 if !fileutils.IsDir(dir) { 61 return nil, errs.New("'%s' is not a directory; cannot use as artifact cache", dir) 62 } 63 64 var artifacts []cachedArtifact 65 infoJson := filepath.Join(dir, constants.ArtifactCacheFileName) 66 if fileutils.FileExists(infoJson) { 67 data, err := fileutils.ReadFile(infoJson) 68 if err != nil { 69 return nil, errs.Wrap(err, "Could not read artifact cache's "+infoJson) 70 } 71 err = json.Unmarshal(data, &artifacts) 72 if err != nil { 73 return nil, errs.Wrap(err, "Unable to read cached artifacts from "+infoJson) 74 } 75 } 76 77 var currentSize int64 = 0 78 artifactMap := map[strfmt.UUID]*cachedArtifact{} 79 for _, artifact := range artifacts { 80 currentSize += artifact.Size 81 artifactMap[artifact.Id] = &cachedArtifact{artifact.Id, artifact.ArchivePath, artifact.Size, artifact.LastAccessTime} 82 } 83 84 logging.Debug("Opened artifact cache at '%s' containing %d artifacts occupying %.1f/%.1f MB", dir, len(artifactMap), float64(currentSize)/float64(MB), float64(maxSize)/float64(MB)) 85 return &ArtifactCache{dir, infoJson, maxSize, currentSize, artifactMap, sync.Mutex{}, 0, 0}, nil 86 } 87 88 // Get returns the path to the cached artifact with the given id along with true if it exists. 89 // Otherwise returns an empty string and false. 90 // Updates the access timestamp if possible so that this artifact is not removed anytime soon. 91 func (cache *ArtifactCache) Get(a strfmt.UUID) (string, bool) { 92 cache.mutex.Lock() 93 defer cache.mutex.Unlock() 94 95 if artifact, found := cache.artifacts[a]; found { 96 logging.Debug("Fetched cached artifact '%s' as '%s'; updating access time", string(a), artifact.ArchivePath) 97 artifact.LastAccessTime = time.Now().Unix() 98 return artifact.ArchivePath, true 99 } 100 return "", false 101 } 102 103 // Stores the given artifact in the cache. 104 // If the cache is too small, removes the least-recently accessed artifacts to make room. 105 func (cache *ArtifactCache) Store(a strfmt.UUID, archivePath string) error { 106 cache.mutex.Lock() 107 defer cache.mutex.Unlock() 108 109 // Replace an existing artifact in the cache. 110 // This would really only happen if a checksum validation fails for the cached artifact (e.g. due 111 // to a bad actor replacing it) and the artifact is silently re-downloaded from the platform. 112 if existingArtifact, found := cache.artifacts[a]; found { 113 path := existingArtifact.ArchivePath 114 logging.Debug("Replacing cached artifact '%s'", path) 115 if fileutils.TargetExists(path) { 116 err := os.Remove(path) 117 if err != nil { 118 return errs.Wrap(err, "Unable to overwrite existing artifact '%s'", path) 119 } 120 } 121 delete(cache.artifacts, existingArtifact.Id) 122 cache.currentSize -= existingArtifact.Size 123 } 124 125 stat, err := os.Stat(archivePath) 126 if err != nil { 127 return errs.Wrap(err, "Unable to stat artifact '%s'. Does it exist?", archivePath) 128 } 129 size := stat.Size() 130 131 if size > cache.maxSize { 132 logging.Debug("Cannot avoid exceeding cache size; not storing artifact") 133 rollbar.Error("Artifact '%s' is %.1fMB, which exceeds the cache size of %.1fMB", a, float64(size)/float64(MB), float64(cache.maxSize)/float64(MB)) 134 return nil 135 } 136 137 for cache.currentSize+size > cache.maxSize { 138 logging.Debug("Storing artifact in cache would exceed cache size; finding least-recently accessed artifact") 139 var lastAccessed *cachedArtifact 140 for _, artifact := range cache.artifacts { 141 if lastAccessed == nil || artifact.LastAccessTime < lastAccessed.LastAccessTime { 142 lastAccessed = artifact 143 } 144 } 145 146 if lastAccessed == nil { 147 rollbar.Error("Cannot avoid exceeding cache size; not storing artifact.") 148 return nil // avoid infinite loop, but this really shouldn't happen... 149 } 150 151 logging.Debug("Removing cached artifact '%s' last accessed on %s", lastAccessed.ArchivePath, time.Unix(lastAccessed.LastAccessTime, 0).Format(time.UnixDate)) 152 if fileutils.TargetExists(lastAccessed.ArchivePath) { 153 err := os.Remove(lastAccessed.ArchivePath) 154 if err != nil { 155 return errs.Wrap(err, "Unable to remove cached artifact '%s'", lastAccessed.ArchivePath) 156 } 157 } 158 delete(cache.artifacts, lastAccessed.Id) 159 cache.currentSize -= lastAccessed.Size 160 } 161 162 targetPath := filepath.Join(cache.dir, string(a)) 163 startTime := time.Now() 164 err = fileutils.CopyFile(archivePath, targetPath) 165 cache.timeSpentCopying += time.Since(startTime) 166 cache.sizeCopied += size 167 if err != nil { 168 return errs.Wrap(err, "Unable to copy artifact '%s' into cache as '%s'", archivePath, targetPath) 169 } 170 171 logging.Debug("Storing artifact '%s'", targetPath) 172 cached := &cachedArtifact{a, targetPath, size, time.Now().Unix()} 173 cache.artifacts[a] = cached 174 cache.currentSize += size 175 176 return nil 177 } 178 179 // Saves this cache's information to disk. 180 // You must call this function when you are done utilizing the cache. 181 func (cache *ArtifactCache) Save() error { 182 artifacts := make([]*cachedArtifact, len(cache.artifacts)) 183 i := 0 184 for _, artifact := range cache.artifacts { 185 artifacts[i] = artifact 186 i++ 187 } 188 data, err := json.Marshal(artifacts) 189 if err != nil { 190 return errs.Wrap(err, "Unable to store cached artifacts into JSON") 191 } 192 193 logging.Debug("Saving artifact cache at '%s'", cache.infoJson) 194 err = fileutils.WriteFile(cache.infoJson, data) 195 if err != nil { 196 return errs.Wrap(err, "Unable to write artifact cache's "+cache.infoJson) 197 } 198 199 if cache.timeSpentCopying > 5*time.Second { 200 multilog.Log(logging.Debug, rollbar.Error)("Spent %.1f seconds copying %.1fMB of artifacts to cache", cache.timeSpentCopying.Seconds(), float64(cache.sizeCopied)/float64(MB)) 201 } 202 cache.timeSpentCopying = 0 // reset 203 cache.sizeCopied = 0 // reset 204 205 return nil 206 }