Use a configuration wrapper to handle loads and saves

This commit is contained in:
Jakob Borg
2014-10-06 09:25:45 +02:00
parent d476c2b613
commit 9b11609b63
12 changed files with 630 additions and 398 deletions

View File

@@ -19,28 +19,26 @@ package config
import (
"encoding/xml"
"fmt"
"io"
"os"
"reflect"
"sort"
"strconv"
"code.google.com/p/go.crypto/bcrypt"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
)
var l = logger.DefaultLogger
type Configuration struct {
Location string `xml:"-" json:"-"`
Version int `xml:"version,attr" default:"5"`
Folders []FolderConfiguration `xml:"folder"`
Devices []DeviceConfiguration `xml:"device"`
GUI GUIConfiguration `xml:"gui"`
Options OptionsConfiguration `xml:"options"`
XMLName xml.Name `xml:"configuration" json:"-"`
Version int `xml:"version,attr" default:"5"`
Folders []FolderConfiguration `xml:"folder"`
Devices []DeviceConfiguration `xml:"device"`
GUI GUIConfiguration `xml:"gui"`
Options OptionsConfiguration `xml:"options"`
XMLName xml.Name `xml:"configuration" json:"-"`
Deprecated_Repositories []FolderConfiguration `xml:"repository" json:"-"`
Deprecated_Nodes []DeviceConfiguration `xml:"node" json:"-"`
@@ -62,6 +60,15 @@ type FolderConfiguration struct {
Deprecated_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"`
}
func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
if r.deviceIDs == nil {
for _, n := range r.Devices {
r.deviceIDs = append(r.deviceIDs, n.DeviceID)
}
}
return r.deviceIDs
}
type VersioningConfiguration struct {
Type string `xml:"type,attr"`
Params map[string]string
@@ -103,15 +110,6 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl
return nil
}
func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
if r.deviceIDs == nil {
for _, n := range r.Devices {
r.deviceIDs = append(r.deviceIDs, n.DeviceID)
}
}
return r.deviceIDs
}
type DeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
@@ -164,6 +162,42 @@ type GUIConfiguration struct {
APIKey string `xml:"apikey,omitempty"`
}
func New(myID protocol.DeviceID) Configuration {
var cfg Configuration
setDefaults(&cfg)
setDefaults(&cfg.Options)
setDefaults(&cfg.GUI)
cfg.prepare(myID)
return cfg
}
func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
var cfg Configuration
setDefaults(&cfg)
setDefaults(&cfg.Options)
setDefaults(&cfg.GUI)
err := xml.NewDecoder(r).Decode(&cfg)
cfg.prepare(myID)
return cfg, err
}
func (cfg *Configuration) WriteXML(w io.Writer) error {
e := xml.NewEncoder(w)
e.Indent("", " ")
err := e.Encode(cfg)
if err != nil {
return err
}
_, err = w.Write([]byte("\n"))
return err
}
func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
for _, n := range cfg.Devices {
@@ -198,117 +232,6 @@ func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
return m
}
func setDefaults(data interface{}) error {
s := reflect.ValueOf(data).Elem()
t := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
tag := t.Field(i).Tag
v := tag.Get("default")
if len(v) > 0 {
switch f.Interface().(type) {
case string:
f.SetString(v)
case int:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
f.SetInt(i)
case bool:
f.SetBool(v == "true")
case []string:
// We don't do anything with string slices here. Any default
// we set will be appended to by the XML decoder, so we fill
// those after decoding.
default:
panic(f.Type())
}
}
}
return nil
}
// fillNilSlices sets default value on slices that are still nil.
func fillNilSlices(data interface{}) error {
s := reflect.ValueOf(data).Elem()
t := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
tag := t.Field(i).Tag
v := tag.Get("default")
if len(v) > 0 {
switch f.Interface().(type) {
case []string:
if f.IsNil() {
rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1)
rv.Index(0).SetString(v)
f.Set(rv)
}
}
}
}
return nil
}
func (cfg *Configuration) Save() error {
fd, err := os.Create(cfg.Location + ".tmp")
if err != nil {
l.Warnln("Saving config:", err)
return err
}
e := xml.NewEncoder(fd)
e.Indent("", " ")
err = e.Encode(cfg)
if err != nil {
fd.Close()
return err
}
_, err = fd.Write([]byte("\n"))
if err != nil {
l.Warnln("Saving config:", err)
fd.Close()
return err
}
err = fd.Close()
if err != nil {
l.Warnln("Saving config:", err)
return err
}
err = osutil.Rename(cfg.Location+".tmp", cfg.Location)
if err != nil {
l.Warnln("Saving config:", err)
}
events.Default.Log(events.ConfigSaved, cfg)
return err
}
func uniqueStrings(ss []string) []string {
var m = make(map[string]bool, len(ss))
for _, s := range ss {
m[s] = true
}
var us = make([]string, 0, len(m))
for k := range m {
us = append(us, k)
}
return us
}
func (cfg *Configuration) prepare(myID protocol.DeviceID) {
fillNilSlices(&cfg.Options)
@@ -356,7 +279,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
cfg.Options.Deprecated_URDeclined = false
cfg.Options.Deprecated_UREnabled = false
// Upgrade to v2 configuration if appropriate
// Upgrade to v1 configuration if appropriate
if cfg.Version == 1 {
convertV1V2(cfg)
}
@@ -421,41 +344,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
}
func New(location string, myID protocol.DeviceID) Configuration {
var cfg Configuration
cfg.Location = location
setDefaults(&cfg)
setDefaults(&cfg.Options)
setDefaults(&cfg.GUI)
cfg.prepare(myID)
return cfg
}
func Load(location string, myID protocol.DeviceID) (Configuration, error) {
var cfg Configuration
cfg.Location = location
setDefaults(&cfg)
setDefaults(&cfg.Options)
setDefaults(&cfg.GUI)
fd, err := os.Open(location)
if err != nil {
return Configuration{}, err
}
err = xml.NewDecoder(fd).Decode(&cfg)
fd.Close()
cfg.prepare(myID)
return cfg, err
}
// ChangeRequiresRestart returns true if updating the configuration requires a
// complete restart.
func ChangeRequiresRestart(from, to Configuration) bool {
@@ -593,29 +481,79 @@ func convertV1V2(cfg *Configuration) {
cfg.Version = 2
}
func setDefaults(data interface{}) error {
s := reflect.ValueOf(data).Elem()
t := s.Type()
type DeviceConfigurationList []DeviceConfiguration
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
tag := t.Field(i).Tag
func (l DeviceConfigurationList) Less(a, b int) bool {
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
}
func (l DeviceConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l DeviceConfigurationList) Len() int {
return len(l)
v := tag.Get("default")
if len(v) > 0 {
switch f.Interface().(type) {
case string:
f.SetString(v)
case int:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
f.SetInt(i)
case bool:
f.SetBool(v == "true")
case []string:
// We don't do anything with string slices here. Any default
// we set will be appended to by the XML decoder, so we fill
// those after decoding.
default:
panic(f.Type())
}
}
}
return nil
}
type FolderDeviceConfigurationList []FolderDeviceConfiguration
// fillNilSlices sets default value on slices that are still nil.
func fillNilSlices(data interface{}) error {
s := reflect.ValueOf(data).Elem()
t := s.Type()
func (l FolderDeviceConfigurationList) Less(a, b int) bool {
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
tag := t.Field(i).Tag
v := tag.Get("default")
if len(v) > 0 {
switch f.Interface().(type) {
case []string:
if f.IsNil() {
rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1)
rv.Index(0).SetString(v)
f.Set(rv)
}
}
}
}
return nil
}
func (l FolderDeviceConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l FolderDeviceConfigurationList) Len() int {
return len(l)
func uniqueStrings(ss []string) []string {
var m = make(map[string]bool, len(ss))
for _, s := range ss {
m[s] = true
}
var us = make([]string, 0, len(m))
for k := range m {
us = append(us, k)
}
return us
}
func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
@@ -664,3 +602,27 @@ loop:
}
return devices[0:count]
}
type DeviceConfigurationList []DeviceConfiguration
func (l DeviceConfigurationList) Less(a, b int) bool {
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
}
func (l DeviceConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l DeviceConfigurationList) Len() int {
return len(l)
}
type FolderDeviceConfigurationList []FolderDeviceConfiguration
func (l FolderDeviceConfigurationList) Less(a, b int) bool {
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
}
func (l FolderDeviceConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l FolderDeviceConfigurationList) Len() int {
return len(l)
}

View File

@@ -52,7 +52,7 @@ func TestDefaultValues(t *testing.T) {
KeepTemporariesH: 24,
}
cfg := New("test", device1)
cfg := New(device1)
if !reflect.DeepEqual(cfg.Options, expected) {
t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
@@ -60,11 +60,12 @@ func TestDefaultValues(t *testing.T) {
}
func TestDeviceConfig(t *testing.T) {
for i, ver := range []string{"v1", "v2", "v3", "v4", "v5"} {
cfg, err := Load("testdata/"+ver+".xml", device1)
for i, ver := range []string{"v3", "v4", "v5"} {
wr, err := Load("testdata/"+ver+".xml", device1)
if err != nil {
t.Error(err)
t.Fatal(err)
}
cfg := wr.cfg
expectedFolders := []FolderConfiguration{
{
@@ -120,8 +121,9 @@ func TestNoListenAddress(t *testing.T) {
}
expected := []string{""}
if !reflect.DeepEqual(cfg.Options.ListenAddress, expected) {
t.Errorf("Unexpected ListenAddress %#v", cfg.Options.ListenAddress)
actual := cfg.Options().ListenAddress
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Unexpected ListenAddress %#v", actual)
}
}
@@ -150,30 +152,30 @@ func TestOverriddenValues(t *testing.T) {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Options, expected) {
if !reflect.DeepEqual(cfg.Options(), expected) {
t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
}
}
func TestDeviceAddressesDynamic(t *testing.T) {
name, _ := os.Hostname()
expected := []DeviceConfiguration{
{
expected := map[protocol.DeviceID]DeviceConfiguration{
device1: {
DeviceID: device1,
Addresses: []string{"dynamic"},
Compression: true,
},
{
device2: {
DeviceID: device2,
Addresses: []string{"dynamic"},
Compression: true,
},
{
device3: {
DeviceID: device3,
Addresses: []string{"dynamic"},
Compression: true,
},
{
device4: {
DeviceID: device4,
Name: name, // Set when auto created
Addresses: []string{"dynamic"},
@@ -185,27 +187,28 @@ func TestDeviceAddressesDynamic(t *testing.T) {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Devices, expected) {
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, cfg.Devices)
actual := cfg.Devices()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, actual)
}
}
func TestDeviceAddressesStatic(t *testing.T) {
name, _ := os.Hostname()
expected := []DeviceConfiguration{
{
expected := map[protocol.DeviceID]DeviceConfiguration{
device1: {
DeviceID: device1,
Addresses: []string{"192.0.2.1", "192.0.2.2"},
},
{
device2: {
DeviceID: device2,
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
},
{
device3: {
DeviceID: device3,
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
},
{
device4: {
DeviceID: device4,
Name: name, // Set when auto created
Addresses: []string{"dynamic"},
@@ -217,8 +220,9 @@ func TestDeviceAddressesStatic(t *testing.T) {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Devices, expected) {
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, cfg.Devices)
actual := cfg.Devices()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Devices differ;\n E: %#v\n A: %#v", expected, actual)
}
}
@@ -228,7 +232,7 @@ func TestVersioningConfig(t *testing.T) {
t.Error(err)
}
vc := cfg.Folders[0].Versioning
vc := cfg.Folders()["test"].Versioning
if vc.Type != "simple" {
t.Errorf(`vc.Type %q != "simple"`, vc.Type)
}
@@ -254,10 +258,11 @@ func TestNewSaveLoad(t *testing.T) {
return err == nil
}
cfg := New(path, device1)
intCfg := New(device1)
cfg := Wrap(path, intCfg)
// To make the equality pass later
cfg.XMLName.Local = "configuration"
cfg.cfg.XMLName.Local = "configuration"
if exists(path) {
t.Error(path, "exists")
@@ -276,20 +281,8 @@ func TestNewSaveLoad(t *testing.T) {
t.Error(err)
}
if !reflect.DeepEqual(cfg, cfg2) {
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2)
}
cfg.GUI.User = "test"
cfg.Save()
cfg2, err = Load(path, device1)
if err != nil {
t.Error(err)
}
if cfg2.GUI.User != "test" || !reflect.DeepEqual(cfg, cfg2) {
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2)
if !reflect.DeepEqual(cfg.Raw(), cfg2.Raw()) {
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg.Raw(), cfg2.Raw())
}
os.Remove(path)

260
internal/config/wrapper.go Normal file
View File

@@ -0,0 +1,260 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
)
// An interface to handle configuration changes, and a wrapper type á la
// http.Handler
type Handler interface {
Changed(Configuration) error
}
type HandlerFunc func(Configuration) error
func (fn HandlerFunc) Changed(cfg Configuration) error {
return fn(cfg)
}
// A wrapper around a Configuration that manages loads, saves and published
// notifications of changes to registered Handlers
type ConfigWrapper struct {
cfg Configuration
path string
deviceMap map[protocol.DeviceID]DeviceConfiguration
folderMap map[string]FolderConfiguration
replaces chan Configuration
subs []Handler
mut sync.Mutex
}
// Wrap wraps an existing Configuration structure and ties it to a file on
// disk.
func Wrap(path string, cfg Configuration) *ConfigWrapper {
w := &ConfigWrapper{cfg: cfg, path: path}
w.replaces = make(chan Configuration)
go w.Serve()
return w
}
// Load loads an existing file on disk and returns a new configuration
// wrapper.
func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
cfg, err := ReadXML(fd, myID)
if err != nil {
return nil, err
}
return Wrap(path, cfg), nil
}
// Serve handles configuration replace events and calls any interested
// handlers. It is started automatically by Wrap() and Load() and should not
// be run manually.
func (w *ConfigWrapper) Serve() {
for cfg := range w.replaces {
w.mut.Lock()
subs := w.subs
w.mut.Unlock()
for _, h := range subs {
h.Changed(cfg)
}
}
}
// Stop stops the Serve() loop.
func (w *ConfigWrapper) Stop() {
close(w.replaces)
}
// Subscriber registers the given handler to be called on any future
// configuration changes.
func (w *ConfigWrapper) Subscribe(h Handler) {
w.mut.Lock()
w.subs = append(w.subs, h)
w.mut.Unlock()
}
// Raw returns the currently wrapped Configuration object.
func (w *ConfigWrapper) Raw() Configuration {
return w.cfg
}
// Replace swaps the current configuration object for the given one.
func (w *ConfigWrapper) Replace(cfg Configuration) {
w.mut.Lock()
defer w.mut.Unlock()
w.cfg = cfg
w.deviceMap = nil
w.folderMap = nil
w.replaces <- cfg
}
// Devices returns a map of devices. Device structures should not be changed,
// other than for the purpose of updating via SetDevice().
func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
if w.deviceMap == nil {
w.deviceMap = make(map[protocol.DeviceID]DeviceConfiguration, len(w.cfg.Devices))
for _, dev := range w.cfg.Devices {
w.deviceMap[dev.DeviceID] = dev
}
}
return w.deviceMap
}
// SetDevice adds a new device to the configuration, or overwrites an existing
// device with the same ID.
func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.deviceMap = nil
for i := range w.cfg.Devices {
if w.cfg.Devices[i].DeviceID == dev.DeviceID {
w.cfg.Devices[i] = dev
w.replaces <- w.cfg
return
}
}
w.cfg.Devices = append(w.cfg.Devices, dev)
w.replaces <- w.cfg
}
// Devices returns a map of folders. Folder structures should not be changed,
// other than for the purpose of updating via SetFolder().
func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
if w.folderMap == nil {
w.folderMap = make(map[string]FolderConfiguration, len(w.cfg.Folders))
for _, fld := range w.cfg.Folders {
w.folderMap[fld.ID] = fld
}
}
return w.folderMap
}
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.folderMap = nil
for i := range w.cfg.Folders {
if w.cfg.Folders[i].ID == fld.ID {
w.cfg.Folders[i] = fld
w.replaces <- w.cfg
return
}
}
w.cfg.Folders = append(w.cfg.Folders, fld)
w.replaces <- w.cfg
}
// Options returns the current options configuration object.
func (w *ConfigWrapper) Options() OptionsConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.Options
}
// SetOptions replaces the current options configuration object.
func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.cfg.Options = opts
w.replaces <- w.cfg
}
// GUI returns the current GUI configuration object.
func (w *ConfigWrapper) GUI() GUIConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.GUI
}
// SetGUI replaces the current GUI configuration object.
func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.cfg.GUI = gui
w.replaces <- w.cfg
}
// InvalidateFolder sets the invalid marker on the given folder.
func (w *ConfigWrapper) InvalidateFolder(id string, err string) {
w.mut.Lock()
defer w.mut.Unlock()
w.folderMap = nil
for i := range w.cfg.Folders {
if w.cfg.Folders[i].ID == id {
w.cfg.Folders[i].Invalid = err
w.replaces <- w.cfg
return
}
}
}
// Save writes the configuration to disk, and generates a ConfigSaved event.
func (w *ConfigWrapper) Save() error {
fd, err := ioutil.TempFile(filepath.Dir(w.path), "cfg")
if err != nil {
return err
}
err = w.cfg.WriteXML(fd)
if err != nil {
fd.Close()
return err
}
err = fd.Close()
if err != nil {
return err
}
events.Default.Log(events.ConfigSaved, w.cfg)
return osutil.Rename(fd.Name(), w.path)
}