Use a configuration wrapper to handle loads and saves
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
260
internal/config/wrapper.go
Normal 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)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ type service interface {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
cfg *config.Configuration
|
||||
cfg *config.ConfigWrapper
|
||||
db *leveldb.DB
|
||||
|
||||
deviceName string
|
||||
@@ -118,7 +118,7 @@ var (
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
// where it sends index information to connected peers and responds to requests
|
||||
// for file data without altering the local folder in any way.
|
||||
func NewModel(cfg *config.Configuration, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
|
||||
func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
|
||||
m := &Model{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
@@ -257,8 +257,8 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
// Returns statistics about each device
|
||||
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
var res = make(map[string]stats.DeviceStatistics)
|
||||
for _, device := range m.cfg.Devices {
|
||||
res[device.DeviceID.String()] = m.deviceStatRef(device.DeviceID).GetStatistics()
|
||||
for id := range m.cfg.Devices() {
|
||||
res[id.String()] = m.deviceStatRef(id).GetStatistics()
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -505,14 +505,14 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
if name := cm.GetOption("name"); name != "" {
|
||||
l.Infof("Device %s name is %q", deviceID, name)
|
||||
device := m.cfg.GetDeviceConfiguration(deviceID)
|
||||
if device != nil && device.Name == "" {
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
if ok && device.Name == "" {
|
||||
device.Name = name
|
||||
m.cfg.Save()
|
||||
m.cfg.SetDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
if m.cfg.GetDeviceConfiguration(deviceID).Introducer {
|
||||
if m.cfg.Devices()[deviceID].Introducer {
|
||||
// This device is an introducer. Go through the announced lists of folders
|
||||
// and devices and add what we are missing.
|
||||
|
||||
@@ -530,12 +530,13 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device.ID)
|
||||
|
||||
if m.cfg.GetDeviceConfiguration(id) == nil {
|
||||
if _, ok := m.cfg.Devices()[id]; !ok {
|
||||
// The device is currently unknown. Add it to the config.
|
||||
|
||||
l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID)
|
||||
newDeviceCfg := config.DeviceConfiguration{
|
||||
DeviceID: id,
|
||||
DeviceID: id,
|
||||
Compression: true,
|
||||
}
|
||||
|
||||
// The introducers' introducers are also our introducers.
|
||||
@@ -544,8 +545,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
newDeviceCfg.Introducer = true
|
||||
}
|
||||
|
||||
m.cfg.Devices = append(m.cfg.Devices, newDeviceCfg)
|
||||
|
||||
m.cfg.SetDevice(newDeviceCfg)
|
||||
changed = true
|
||||
}
|
||||
|
||||
@@ -565,10 +565,11 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
m.deviceFolders[id] = append(m.deviceFolders[id], folder.ID)
|
||||
m.folderDevices[folder.ID] = append(m.folderDevices[folder.ID], id)
|
||||
|
||||
folderCfg := m.cfg.GetFolderConfiguration(folder.ID)
|
||||
folderCfg := m.cfg.Folders()[folder.ID]
|
||||
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
|
||||
DeviceID: id,
|
||||
})
|
||||
m.cfg.SetFolder(folderCfg)
|
||||
|
||||
changed = true
|
||||
}
|
||||
@@ -973,7 +974,7 @@ func (m *Model) ScanFolders() {
|
||||
go func() {
|
||||
err := m.ScanFolder(folder)
|
||||
if err != nil {
|
||||
invalidateFolder(m.cfg, folder, err)
|
||||
m.cfg.InvalidateFolder(folder, err.Error())
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -1131,7 +1132,7 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe
|
||||
ID: device[:],
|
||||
Flags: protocol.FlagShareTrusted,
|
||||
}
|
||||
if deviceCfg := m.cfg.GetDeviceConfiguration(device); deviceCfg.Introducer {
|
||||
if deviceCfg := m.cfg.Devices()[device]; deviceCfg.Introducer {
|
||||
cn.Flags |= protocol.FlagIntroducer
|
||||
}
|
||||
cr.Devices = append(cr.Devices, cn)
|
||||
|
||||
@@ -68,7 +68,7 @@ func init() {
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db)
|
||||
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
|
||||
m.ScanFolder("default")
|
||||
|
||||
@@ -258,7 +258,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
ClientVersion: "v0.9.4",
|
||||
}
|
||||
|
||||
cfg := config.New("/tmp/test", device1)
|
||||
cfg := config.New(device1)
|
||||
cfg.Devices = []config.DeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
@@ -266,7 +266,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
}
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(&cfg, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db)
|
||||
if cfg.Devices[0].Name != "" {
|
||||
t.Errorf("Device already has a name")
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClusterConfig(t *testing.T) {
|
||||
cfg := config.New("/tmp/test", device1)
|
||||
cfg := config.New(device1)
|
||||
cfg.Devices = []config.DeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
@@ -324,7 +324,7 @@ func TestClusterConfig(t *testing.T) {
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
|
||||
m := NewModel(&cfg, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db)
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
m.AddFolder(cfg.Folders[1])
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ loop:
|
||||
}
|
||||
p.model.setState(p.folder, FolderScanning)
|
||||
if err := p.model.ScanFolder(p.folder); err != nil {
|
||||
invalidateFolder(p.model.cfg, p.folder, err)
|
||||
p.model.cfg.InvalidateFolder(p.folder, err.Error())
|
||||
break loop
|
||||
}
|
||||
p.model.setState(p.folder, FolderIdle)
|
||||
@@ -687,7 +687,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
|
||||
|
||||
// clean deletes orphaned temporary files
|
||||
func (p *Puller) clean() {
|
||||
keep := time.Duration(p.model.cfg.Options.KeepTemporariesH) * time.Hour
|
||||
keep := time.Duration(p.model.cfg.Options().KeepTemporariesH) * time.Hour
|
||||
now := time.Now()
|
||||
filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestHandleFile(t *testing.T) {
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db)
|
||||
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
|
||||
m.updateLocal("default", existingFile)
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(&config.Configuration{}, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db)
|
||||
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
|
||||
m.updateLocal("default", existingFile)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func (s *Scanner) Serve() {
|
||||
|
||||
s.model.setState(s.folder, FolderScanning)
|
||||
if err := s.model.ScanFolder(s.folder); err != nil {
|
||||
invalidateFolder(s.model.cfg, s.folder, err)
|
||||
s.model.cfg.InvalidateFolder(s.folder, err.Error())
|
||||
return
|
||||
}
|
||||
s.model.setState(s.folder, FolderIdle)
|
||||
|
||||
@@ -17,12 +17,17 @@
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrNoHome = errors.New("No home directory found - set $HOME (or the platform equivalent).")
|
||||
|
||||
// Try to keep this entire operation atomic-like. We shouldn't be doing this
|
||||
// often enough that there is any contention on this lock.
|
||||
var renameLock sync.Mutex
|
||||
@@ -81,3 +86,40 @@ func InWritableDir(fn func(string) error, path string) error {
|
||||
|
||||
return fn(path)
|
||||
}
|
||||
|
||||
func ExpandTilde(path string) (string, error) {
|
||||
if path == "~" {
|
||||
return getHomeDir()
|
||||
}
|
||||
|
||||
path = filepath.FromSlash(path)
|
||||
if !strings.HasPrefix(path, fmt.Sprintf("~%c", os.PathSeparator)) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, path[2:]), nil
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
var home string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
|
||||
if home == "" {
|
||||
home = os.Getenv("UserProfile")
|
||||
}
|
||||
default:
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
|
||||
if home == "" {
|
||||
return "", ErrNoHome
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user