3cb7734eac
Before this change we assumed that github.com/Unknwon/goconfig was threadsafe as documented. However it turns out it is not threadsafe and looking at the code it appears that making it threadsafe might be quite hard. So this change increases the lock coverage in configfile to cover the goconfig uses also. Fixes #6378
263 lines
6.4 KiB
Go
263 lines
6.4 KiB
Go
// Package configfile implements a config file loader and saver
|
|
package configfile
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Unknwon/goconfig"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/lib/file"
|
|
)
|
|
|
|
// Install installs the config file handler
|
|
func Install() {
|
|
config.SetData(&Storage{})
|
|
}
|
|
|
|
// Storage implements config.Storage for saving and loading config
|
|
// data in a simple INI based file.
|
|
type Storage struct {
|
|
mu sync.Mutex // to protect the following variables
|
|
gc *goconfig.ConfigFile // config file loaded - not thread safe
|
|
fi os.FileInfo // stat of the file when last loaded
|
|
}
|
|
|
|
// Check to see if we need to reload the config
|
|
//
|
|
// mu must be held when calling this
|
|
func (s *Storage) _check() {
|
|
if configPath := config.GetConfigPath(); configPath != "" {
|
|
// Check to see if config file has changed since it was last loaded
|
|
fi, err := os.Stat(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 externally - 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{}))
|
|
}
|
|
}()
|
|
|
|
configPath := config.GetConfigPath()
|
|
if configPath == "" {
|
|
return config.ErrorConfigFileNotFound
|
|
}
|
|
|
|
fd, err := os.Open(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(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()
|
|
|
|
configPath := config.GetConfigPath()
|
|
if configPath == "" {
|
|
return fmt.Errorf("failed to save config file, path is empty")
|
|
}
|
|
|
|
dir, name := filepath.Split(configPath)
|
|
err := file.MkdirAll(dir, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
f, err := ioutil.TempFile(dir, name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp file for new config: %w", 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 fmt.Errorf("failed to save config file: %w", err)
|
|
}
|
|
|
|
if err := config.Encrypt(&buf, f); err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = f.Sync()
|
|
err = f.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to close config file: %w", err)
|
|
}
|
|
|
|
var fileMode os.FileMode = 0600
|
|
info, err := os.Stat(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(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(configPath, configPath+".old"); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to move previous config to backup location: %w", err)
|
|
}
|
|
if err = os.Rename(f.Name(), configPath); err != nil {
|
|
return fmt.Errorf("failed to move newly written config from %s to final location: %v", f.Name(), err)
|
|
}
|
|
if err := os.Remove(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(configPath)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Serialize the config into a string
|
|
func (s *Storage) Serialize() (string, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
var buf bytes.Buffer
|
|
if err := goconfig.SaveConfigData(s.gc, &buf); err != nil {
|
|
return "", fmt.Errorf("failed to save config file: %w", err)
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// HasSection returns true if section exists in the config file
|
|
func (s *Storage) HasSection(section string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
_, err := s.gc.GetSection(section)
|
|
return err == nil
|
|
}
|
|
|
|
// DeleteSection removes the named section and all config from the
|
|
// config file
|
|
func (s *Storage) DeleteSection(section string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
s.gc.DeleteSection(section)
|
|
}
|
|
|
|
// GetSectionList returns a slice of strings with names for all the
|
|
// sections
|
|
func (s *Storage) GetSectionList() []string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
return s.gc.GetSectionList()
|
|
}
|
|
|
|
// GetKeyList returns the keys in this section
|
|
func (s *Storage) GetKeyList(section string) []string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
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.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
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.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
if strings.HasPrefix(section, ":") {
|
|
fs.Logf(nil, "Can't save config %q for on the fly backend %q", key, section)
|
|
return
|
|
}
|
|
s.gc.SetValue(section, key, value)
|
|
}
|
|
|
|
// DeleteKey removes the key under section
|
|
func (s *Storage) DeleteKey(section string, key string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s._check()
|
|
return s.gc.DeleteKey(section, key)
|
|
}
|
|
|
|
// Check the interface is satisfied
|
|
var _ config.Storage = (*Storage)(nil)
|