1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package trust
5
6import (
7	"context"
8	"encoding/json"
9	"io/ioutil"
10	"os"
11	"path/filepath"
12
13	"github.com/zeebo/errs"
14
15	"storj.io/common/fpath"
16)
17
18// Cache caches source information about trusted satellites.
19type Cache struct {
20	path string
21	data *CacheData
22}
23
24// LoadCache loads a cache from a file on disk. If the file is not present, the
25// cache is still loaded.  If the file cannot be read for any other reason, the
26// function will return an error. LoadCache ensures the containing directory
27// exists.
28func LoadCache(path string) (*Cache, error) {
29	if path == "" {
30		return nil, Error.New("cache path cannot be empty")
31	}
32	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
33		return nil, Error.New("unable to make cache parent directory: %w", err)
34	}
35
36	data, err := LoadCacheData(path)
37	switch {
38	case err == nil:
39	case errs.IsFunc(err, os.IsNotExist):
40		data = NewCacheData()
41	default:
42		return nil, err
43	}
44
45	return &Cache{
46		path: path,
47		data: data,
48	}, nil
49}
50
51// Path returns the path on disk to the file containing the cache.
52func (cache *Cache) Path() string {
53	return cache.path
54}
55
56// Lookup takes a cache key and returns entries associated with that key. If
57// the key is unset in the cache, false is returned for ok. Otherwise the
58// entries are returned with ok returned as true.
59func (cache *Cache) Lookup(key string) (entries []Entry, ok bool) {
60	entries, ok = cache.data.Entries[key]
61	return entries, ok
62}
63
64// Set sets the entries in the cache for the provided key.
65func (cache *Cache) Set(key string, entries []Entry) {
66	cache.data.Entries[key] = entries
67}
68
69// Save persists the cache to disk.
70func (cache *Cache) Save(ctx context.Context) (err error) {
71	defer mon.Task()(&ctx)(&err)
72	return SaveCacheData(cache.path, cache.data)
73}
74
75// CacheData represents the data stored in the cache.
76type CacheData struct {
77	Entries map[string][]Entry `json:"entries"`
78}
79
80// NewCacheData returns an new CacheData.
81func NewCacheData() *CacheData {
82	return &CacheData{
83		Entries: make(map[string][]Entry),
84	}
85}
86
87// LoadCacheData loads the cache data from the given path.
88func LoadCacheData(path string) (*CacheData, error) {
89	dataBytes, err := ioutil.ReadFile(path)
90	if err != nil {
91		return nil, Error.Wrap(err)
92	}
93
94	data := NewCacheData()
95	if err := json.Unmarshal(dataBytes, data); err != nil {
96		return nil, Error.New("malformed cache: %w", err)
97	}
98	// Ensure the entries map is always non-nil on load
99	if data.Entries == nil {
100		data.Entries = map[string][]Entry{}
101	}
102	return data, nil
103}
104
105// SaveCacheData persists the cache data to the given path.
106func SaveCacheData(path string, data *CacheData) error {
107	// Ensure the entries map is always non-nil on save
108	if data.Entries == nil {
109		data.Entries = map[string][]Entry{}
110	}
111	dataBytes, err := json.MarshalIndent(data, "", "  ")
112	if err != nil {
113		return Error.Wrap(err)
114	}
115	return fpath.AtomicWriteFile(path, dataBytes, 0644)
116}
117