mirror of
https://github.com/rclone/rclone.git
synced 2024-11-25 18:04:55 +01:00
298 lines
7.5 KiB
Go
298 lines
7.5 KiB
Go
// Package configfile implements a config file loader and saver
|
|
package configfile
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Unknwon/goconfig" //nolint:misspell // Don't include misspell when running golangci-lint
|
|
"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")
|
|
}
|
|
configDir, configName := filepath.Split(configPath)
|
|
|
|
info, err := os.Lstat(configPath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to resolve config file path: %w", err)
|
|
}
|
|
} else {
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
configPath, err = os.Readlink(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve config file symbolic link: %w", err)
|
|
}
|
|
if !filepath.IsAbs(configPath) {
|
|
configPath = filepath.Join(configDir, configPath)
|
|
}
|
|
configDir = filepath.Dir(configPath)
|
|
}
|
|
}
|
|
err = file.MkdirAll(configDir, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
f, err := os.CreateTemp(configDir, configName)
|
|
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 file for new config: %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)
|
|
}
|
|
|
|
fbackup, err := os.CreateTemp(configDir, configName+".old")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp file for old config backup: %w", err)
|
|
}
|
|
err = fbackup.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to close temp file for old config backup: %w", err)
|
|
}
|
|
keepBackup := true
|
|
defer func() {
|
|
if !keepBackup {
|
|
if err := os.Remove(fbackup.Name()); err != nil && !os.IsNotExist(err) {
|
|
fs.Errorf(nil, "Failed to remove temp file for old config backup: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if err = os.Rename(configPath, fbackup.Name()); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to move previous config to backup location: %w", err)
|
|
}
|
|
keepBackup = false // no existing file, no need to keep backup even if writing of new file fails
|
|
}
|
|
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)
|
|
}
|
|
keepBackup = false // new file was written, no need to keep backup
|
|
|
|
// 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)
|