Files
backrest/internal/api/syncapi/remoteconfigstore.go

171 lines
4.3 KiB
Go

package syncapi
import (
"errors"
"fmt"
"hash/crc32"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
"google.golang.org/protobuf/encoding/protojson"
)
var (
sanitizeFilenameRegex = regexp.MustCompile("[^a-zA-Z0-9\\-_\\.]+")
ErrRemoteConfigNotFound = errors.New("remote config not found")
)
type RemoteConfigStore interface {
// Get a remote config for the given instance ID.
Get(instanceID string) (*v1.RemoteConfig, error)
// Update or create a remote config for the given instance ID.
Update(instanceID string, config *v1.RemoteConfig) error
// Delete a remote config for the given instance ID.
Delete(instanceID string) error
}
type jsonDirRemoteConfigStore struct {
mu sync.Mutex
dir string
cache map[string]*v1.RemoteConfig
}
func NewJSONDirRemoteConfigStore(dir string) RemoteConfigStore {
return &jsonDirRemoteConfigStore{
dir: dir,
cache: make(map[string]*v1.RemoteConfig),
}
}
func (s *jsonDirRemoteConfigStore) Get(instanceID string) (*v1.RemoteConfig, error) {
s.mu.Lock()
defer s.mu.Unlock()
if instanceID == "" {
return nil, errors.New("instanceID is required")
}
if config, ok := s.cache[instanceID]; ok {
return config, nil
}
file := s.fileForInstance(instanceID)
data, err := os.ReadFile(file)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrRemoteConfigNotFound
}
return nil, fmt.Errorf("read config file: %w", err)
}
var config v1.RemoteConfig
if err = (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
s.cache[instanceID] = &config
return &config, nil
}
func (s *jsonDirRemoteConfigStore) Update(instanceID string, config *v1.RemoteConfig) error {
s.mu.Lock()
defer s.mu.Unlock()
if instanceID == "" {
return errors.New("instanceID is required")
}
file := s.fileForInstance(instanceID)
data, err := protojson.MarshalOptions{
Indent: " ",
Multiline: true,
}.Marshal(config)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
err = os.MkdirAll(filepath.Dir(file), 0755)
if err != nil {
return fmt.Errorf("create config directory: %w", err)
}
err = os.WriteFile(file, data, 0600)
if err != nil {
return fmt.Errorf("write config file: %w", err)
}
s.cache[instanceID] = config
return nil
}
func (s *jsonDirRemoteConfigStore) Delete(instanceID string) error {
s.mu.Lock()
defer s.mu.Unlock()
if instanceID == "" {
return errors.New("instanceID is required")
}
file := s.fileForInstance(instanceID)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove config file: %w", err)
}
delete(s.cache, instanceID)
return nil
}
func (s *jsonDirRemoteConfigStore) fileForInstance(instanceID string) string {
safeInstanceID := strings.Replace(instanceID, "..", ".", -1)
safeInstanceID = sanitizeFilenameRegex.ReplaceAllString(safeInstanceID, "_")
checksum := crc32.ChecksumIEEE([]byte(instanceID)) // checksum eliminates collisions in the case of replacing characters.
return filepath.Join(s.dir, fmt.Sprintf("%s-%08x.json", safeInstanceID, checksum))
}
type memoryConfigStore struct {
configs map[string]*v1.RemoteConfig
}
func newMemoryConfigStore() *memoryConfigStore {
return &memoryConfigStore{
configs: make(map[string]*v1.RemoteConfig),
}
}
func (s *memoryConfigStore) Get(instanceID string) (*v1.RemoteConfig, error) {
if config, ok := s.configs[instanceID]; ok {
return config, nil
}
return nil, ErrRemoteConfigNotFound
}
func (s *memoryConfigStore) Update(instanceID string, config *v1.RemoteConfig) error {
if instanceID == "" {
return errors.New("instanceID is required")
}
s.configs[instanceID] = config
return nil
}
func (s *memoryConfigStore) Delete(instanceID string) error {
if instanceID == "" {
return errors.New("instanceID is required")
}
delete(s.configs, instanceID)
return nil
}
func GetRepoConfig(store RemoteConfigStore, instanceID, repoID string) (*v1.RemoteRepo, error) {
config, err := store.Get(instanceID)
if err != nil {
return nil, fmt.Errorf("get %q: %w", instanceID, err)
}
for _, repo := range config.Repos {
if repo.Id == repoID {
return repo, nil
}
}
return nil, fmt.Errorf("get %q/%q: %w", instanceID, repoID, ErrRemoteConfigNotFound)
}