databricks-cli/cmd/labs/localcache/jsonfile.go

110 lines
2.7 KiB
Go

package localcache
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"time"
"github.com/databricks/cli/libs/log"
)
const userRW = 0o600
const ownerRWXworldRX = 0o755
func NewLocalCache[T any](dir, name string, validity time.Duration) LocalCache[T] {
return LocalCache[T]{
dir: dir,
name: name,
validity: validity,
}
}
type LocalCache[T any] struct {
name string
dir string
validity time.Duration
zero T
}
func (r *LocalCache[T]) Load(ctx context.Context, refresh func() (T, error)) (T, error) {
cached, err := r.loadCache()
if errors.Is(err, fs.ErrNotExist) {
return r.refreshCache(ctx, refresh, r.zero)
} else if err != nil {
return r.zero, err
} else if time.Since(cached.Refreshed) > r.validity {
return r.refreshCache(ctx, refresh, cached.Data)
}
return cached.Data, nil
}
type cached[T any] struct {
// we don't use mtime of the file because it's easier to
// for testdata used in the unit tests to be somewhere far
// in the future and don't bother about switching the mtime bit.
Refreshed time.Time `json:"refreshed_at"`
Data T `json:"data"`
}
func (r *LocalCache[T]) refreshCache(ctx context.Context, refresh func() (T, error), offlineVal T) (T, error) {
data, err := refresh()
var urlError *url.Error
if errors.As(err, &urlError) {
log.Warnf(ctx, "System offline. Cannot refresh cache: %s", urlError)
return offlineVal, nil
}
if err != nil {
return r.zero, fmt.Errorf("refresh: %w", err)
}
return r.writeCache(ctx, data)
}
func (r *LocalCache[T]) writeCache(ctx context.Context, data T) (T, error) {
cached := &cached[T]{time.Now(), data}
raw, err := json.MarshalIndent(cached, "", " ")
if err != nil {
return r.zero, fmt.Errorf("json marshal: %w", err)
}
cacheFile := r.FileName()
err = os.WriteFile(cacheFile, raw, userRW)
if errors.Is(err, fs.ErrNotExist) {
cacheDir := filepath.Dir(cacheFile)
err := os.MkdirAll(cacheDir, ownerRWXworldRX)
if err != nil {
return r.zero, fmt.Errorf("create %s: %w", cacheDir, err)
}
err = os.WriteFile(cacheFile, raw, userRW)
if err != nil {
return r.zero, fmt.Errorf("retry save cache: %w", err)
}
return data, nil
} else if err != nil {
return r.zero, fmt.Errorf("save cache: %w", err)
}
return data, nil
}
func (r *LocalCache[T]) FileName() string {
return filepath.Join(r.dir, fmt.Sprintf("%s.json", r.name))
}
func (r *LocalCache[T]) loadCache() (*cached[T], error) {
jsonFile := r.FileName()
raw, err := os.ReadFile(r.FileName())
if err != nil {
return nil, fmt.Errorf("read %s: %w", jsonFile, err)
}
var v cached[T]
err = json.Unmarshal(raw, &v)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", jsonFile, err)
}
return &v, nil
}