rclone/fs/config/configfile/configfile.go
Nick Craig-Wood 46078d391f config: make config file reads reload the config file if needed #4996
Before this change the config file needed to be explicitly reloaded.
This coupled the config file implementation with the backends
needlessly.

This change stats the config file to see if it needs to be reloaded on
every config file operation.

This allows us to remove calls to

- config.SaveConfig
- config.GetFresh

Which now makes the the only needed interface to the config file be
that provided by configmap.Map when rclone is not being configured.

This also adds tests for configfile
2021-03-14 16:03:35 +00:00

227 lines
5.8 KiB
Go

// Package configfile implements a config file loader and saver
package configfile
import (
"bytes"
"context"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/Unknwon/goconfig"
"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
)
// LoadConfig installs the config file handler and calls config.LoadConfig
func LoadConfig(ctx context.Context) {
config.Data = &Storage{}
config.LoadConfig(ctx)
}
// Storage implements config.Storage for saving and loading config
// data in a simple INI based file.
type Storage struct {
gc *goconfig.ConfigFile // config file loaded - thread safe
mu sync.Mutex // to protect the following variables
fi os.FileInfo // stat of the file when last loaded
}
// Check to see if we need to reload the config
func (s *Storage) check() {
s.mu.Lock()
defer s.mu.Unlock()
// Check to see if config file has changed since it was last loaded
fi, err := os.Stat(config.ConfigPath)
if err == nil {
// check to see if config file has changed and if it has, reload it
if s.fi == nil || !fi.ModTime().Equal(s.fi.ModTime()) || fi.Size() != s.fi.Size() {
fs.Debugf(nil, "Config file has changed externaly - reloading")
err := s._load()
if err != nil {
fs.Errorf(nil, "Failed to read config file - using previous config: %v", err)
}
}
}
}
// _load the config from permanent storage, decrypting if necessary
//
// mu must be held when calling this
func (s *Storage) _load() (err error) {
// Make sure we have a sensible default even when we error
defer func() {
if s.gc == nil {
s.gc, _ = goconfig.LoadFromReader(bytes.NewReader([]byte{}))
}
}()
fd, err := os.Open(config.ConfigPath)
if err != nil {
if os.IsNotExist(err) {
return config.ErrorConfigFileNotFound
}
return err
}
defer fs.CheckClose(fd, &err)
// Update s.fi with the current file info
s.fi, _ = os.Stat(config.ConfigPath)
cryptReader, err := config.Decrypt(fd)
if err != nil {
return err
}
gc, err := goconfig.LoadFromReader(cryptReader)
if err != nil {
return err
}
s.gc = gc
return nil
}
// Load the config from permanent storage, decrypting if necessary
func (s *Storage) Load() (err error) {
s.mu.Lock()
defer s.mu.Unlock()
return s._load()
}
// Save the config to permanent storage, encrypting if necessary
func (s *Storage) Save() error {
s.mu.Lock()
defer s.mu.Unlock()
dir, name := filepath.Split(config.ConfigPath)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return errors.Wrap(err, "failed to create config directory")
}
f, err := ioutil.TempFile(dir, name)
if err != nil {
return errors.Errorf("Failed to create temp file for new config: %v", err)
}
defer func() {
_ = f.Close()
if err := os.Remove(f.Name()); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove temp config file: %v", err)
}
}()
var buf bytes.Buffer
if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
return errors.Errorf("Failed to save config file: %v", err)
}
if err := config.Encrypt(&buf, f); err != nil {
return err
}
_ = f.Sync()
err = f.Close()
if err != nil {
return errors.Errorf("Failed to close config file: %v", err)
}
var fileMode os.FileMode = 0600
info, err := os.Stat(config.ConfigPath)
if err != nil {
fs.Debugf(nil, "Using default permissions for config file: %v", fileMode)
} else if info.Mode() != fileMode {
fs.Debugf(nil, "Keeping previous permissions for config file: %v", info.Mode())
fileMode = info.Mode()
}
attemptCopyGroup(config.ConfigPath, f.Name())
err = os.Chmod(f.Name(), fileMode)
if err != nil {
fs.Errorf(nil, "Failed to set permissions on config file: %v", err)
}
if err = os.Rename(config.ConfigPath, config.ConfigPath+".old"); err != nil && !os.IsNotExist(err) {
return errors.Errorf("Failed to move previous config to backup location: %v", err)
}
if err = os.Rename(f.Name(), config.ConfigPath); err != nil {
return errors.Errorf("Failed to move newly written config from %s to final location: %v", f.Name(), err)
}
if err := os.Remove(config.ConfigPath + ".old"); err != nil && !os.IsNotExist(err) {
fs.Errorf(nil, "Failed to remove backup config file: %v", err)
}
// Update s.fi with the newly written file
s.fi, _ = os.Stat(config.ConfigPath)
return nil
}
// Serialize the config into a string
func (s *Storage) Serialize() (string, error) {
s.check()
var buf bytes.Buffer
if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
return "", errors.Errorf("Failed to save config file: %v", err)
}
return buf.String(), nil
}
// HasSection returns true if section exists in the config file
func (s *Storage) HasSection(section string) bool {
s.check()
_, err := s.gc.GetSection(section)
if err != nil {
return false
}
return true
}
// DeleteSection removes the named section and all config from the
// config file
func (s *Storage) DeleteSection(section string) {
s.check()
s.gc.DeleteSection(section)
}
// GetSectionList returns a slice of strings with names for all the
// sections
func (s *Storage) GetSectionList() []string {
s.check()
return s.gc.GetSectionList()
}
// GetKeyList returns the keys in this section
func (s *Storage) GetKeyList(section string) []string {
s.check()
return s.gc.GetKeyList(section)
}
// GetValue returns the key in section with a found flag
func (s *Storage) GetValue(section string, key string) (value string, found bool) {
s.check()
value, err := s.gc.GetValue(section, key)
if err != nil {
return "", false
}
return value, true
}
// SetValue sets the value under key in section
func (s *Storage) SetValue(section string, key string, value string) {
s.check()
s.gc.SetValue(section, key, value)
}
// DeleteKey removes the key under section
func (s *Storage) DeleteKey(section string, key string) bool {
s.check()
return s.gc.DeleteKey(section, key)
}
// Check the interface is satisfied
var _ config.Storage = (*Storage)(nil)