This repository has been archived on 2024-05-03. You can view files and clone it, but cannot push or open issues or pull requests.
treefmt/internal/cache/cache.go
Brian McGee 4c45d2aa7e feat: allow missing formatters (#6)
Closes #3

Reviewed-on: #6
Co-authored-by: Brian McGee <brian@bmcgee.ie>
Co-committed-by: Brian McGee <brian@bmcgee.ie>
2023-12-23 15:00:39 +00:00

171 lines
3.4 KiB
Go

package cache
import (
"context"
"crypto/sha1"
"encoding/base32"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/juju/errors"
"github.com/vmihailenco/msgpack/v5"
bolt "go.etcd.io/bbolt"
)
const (
modifiedBucket = "modified"
)
var db *bolt.DB
func Open(treeRoot string, clean bool) (err error) {
// determine a unique and consistent db name for the tree root
h := sha1.New()
h.Write([]byte(treeRoot))
digest := h.Sum(nil)
name := base32.StdEncoding.EncodeToString(digest)
path, err := xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name))
// bust the cache if specified
if clean {
err := os.Remove(path)
if errors.Is(err, os.ErrNotExist) {
err = nil
} else if err != nil {
return errors.Annotate(err, "failed to clear cache")
}
}
if err != nil {
return errors.Annotate(err, "could not resolve local path for the cache")
}
db, err = bolt.Open(path, 0o600, nil)
if err != nil {
return errors.Annotate(err, "failed to open cache")
}
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucket([]byte(modifiedBucket))
if errors.Is(err, bolt.ErrBucketExists) {
return nil
}
return err
})
return
}
func Close() error {
if db == nil {
return nil
}
return db.Close()
}
func getFileInfo(bucket *bolt.Bucket, path string) (*FileInfo, error) {
b := bucket.Get([]byte(path))
if b != nil {
var cached FileInfo
if err := msgpack.Unmarshal(b, &cached); err != nil {
return nil, errors.Annotatef(err, "failed to unmarshal cache info for path '%v'", path)
}
return &cached, nil
} else {
return nil, nil
}
}
func ChangeSet(ctx context.Context, root string, pathsCh chan<- string) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(modifiedBucket))
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return errors.Annotate(err, "failed to walk path")
} else if ctx.Err() != nil {
return ctx.Err()
} else if info.IsDir() {
// todo what about symlinks?
return nil
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
// skip symlinks
return nil
}
cached, err := getFileInfo(bucket, path)
if err != nil {
return err
}
changedOrNew := cached == nil || !(cached.Modified == info.ModTime() && cached.Size == info.Size())
if !changedOrNew {
// no change
return nil
}
// pass on the path
pathsCh <- path
return nil
})
})
}
func Update(paths []string) (int, error) {
if len(paths) == 0 {
return 0, nil
}
var changes int
return changes, db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(modifiedBucket))
for _, path := range paths {
if path == "" {
continue
}
cached, err := getFileInfo(bucket, path)
if err != nil {
return err
}
pathInfo, err := os.Stat(path)
if err != nil {
return err
}
if cached == nil || !(cached.Modified == pathInfo.ModTime() && cached.Size == pathInfo.Size()) {
changes += 1
} else {
// no change to write
continue
}
cacheInfo := FileInfo{
Size: pathInfo.Size(),
Modified: pathInfo.ModTime(),
}
bytes, err := msgpack.Marshal(cacheInfo)
if err != nil {
return errors.Annotate(err, "failed to marshal mod time")
}
if err = bucket.Put([]byte(path), bytes); err != nil {
return errors.Annotate(err, "failed to put mode time")
}
}
return nil
})
}