mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
gui, lib/config, lib/model: Support auto-accepting folders (fixes #2299)
Also introduces a new Waiter interface for config changes and segments the configuration GUI. GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4551
This commit is contained in:
parent
c005b8dcb0
commit
445c4edeca
@ -112,12 +112,12 @@ type configIntf interface {
|
|||||||
GUI() config.GUIConfiguration
|
GUI() config.GUIConfiguration
|
||||||
RawCopy() config.Configuration
|
RawCopy() config.Configuration
|
||||||
Options() config.OptionsConfiguration
|
Options() config.OptionsConfiguration
|
||||||
Replace(cfg config.Configuration) error
|
Replace(cfg config.Configuration) (config.Waiter, error)
|
||||||
Subscribe(c config.Committer)
|
Subscribe(c config.Committer)
|
||||||
Folders() map[string]config.FolderConfiguration
|
Folders() map[string]config.FolderConfiguration
|
||||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||||
SetDevice(config.DeviceConfiguration) error
|
SetDevice(config.DeviceConfiguration) (config.Waiter, error)
|
||||||
SetDevices([]config.DeviceConfiguration) error
|
SetDevices([]config.DeviceConfiguration) (config.Waiter, error)
|
||||||
Save() error
|
Save() error
|
||||||
ListenAddresses() []string
|
ListenAddresses() []string
|
||||||
RequiresRestart() bool
|
RequiresRestart() bool
|
||||||
@ -809,7 +809,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Activate and save
|
// Activate and save
|
||||||
|
|
||||||
if err := s.cfg.Replace(to); err != nil {
|
if _, err := s.cfg.Replace(to); err != nil {
|
||||||
l.Warnln("Replacing config:", err)
|
l.Warnln("Replacing config:", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -1201,7 +1201,7 @@ func (s *apiService) makeDevicePauseHandler(paused bool) http.HandlerFunc {
|
|||||||
cfgs = append(cfgs, cfg)
|
cfgs = append(cfgs, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.cfg.SetDevices(cfgs); err != nil {
|
if _, err := s.cfg.SetDevices(cfgs); err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1080,14 +1080,7 @@ func defaultConfig(cfgFile string) *config.Wrapper {
|
|||||||
|
|
||||||
if !noDefaultFolder {
|
if !noDefaultFolder {
|
||||||
l.Infoln("Default folder created and/or linked to new config")
|
l.Infoln("Default folder created and/or linked to new config")
|
||||||
defaultFolder = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, locations[locDefFolder])
|
defaultFolder = config.NewFolderConfiguration(myID, "default", "Default Folder", fs.FilesystemTypeBasic, locations[locDefFolder])
|
||||||
defaultFolder.Label = "Default Folder"
|
|
||||||
defaultFolder.RescanIntervalS = 60
|
|
||||||
defaultFolder.FSWatcherDelayS = 10
|
|
||||||
defaultFolder.MinDiskFree = config.Size{Value: 1, Unit: "%"}
|
|
||||||
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
|
|
||||||
defaultFolder.AutoNormalize = true
|
|
||||||
defaultFolder.MaxConflicts = -1
|
|
||||||
} else {
|
} else {
|
||||||
l.Infoln("We will skip creation of a default folder on first start since the proper envvar is set")
|
l.Infoln("We will skip creation of a default folder on first start since the proper envvar is set")
|
||||||
}
|
}
|
||||||
@ -1331,7 +1324,7 @@ func setPauseState(cfg *config.Wrapper, paused bool) {
|
|||||||
for i := range raw.Folders {
|
for i := range raw.Folders {
|
||||||
raw.Folders[i].Paused = paused
|
raw.Folders[i].Paused = paused
|
||||||
}
|
}
|
||||||
if err := cfg.Replace(raw); err != nil {
|
if _, err := cfg.Replace(raw); err != nil {
|
||||||
l.Fatalln("Cannot adjust paused state:", err)
|
l.Fatalln("Cannot adjust paused state:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,8 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
|
|||||||
return config.OptionsConfiguration{}
|
return config.OptionsConfiguration{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) Replace(cfg config.Configuration) error {
|
func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||||
@ -48,12 +48,12 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) error {
|
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) error {
|
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) (config.Waiter, error) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockedConfig) Save() error {
|
func (c *mockedConfig) Save() error {
|
||||||
|
@ -367,3 +367,7 @@ ul.three-columns li, ul.two-columns li {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
@ -28,8 +28,10 @@
|
|||||||
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
|
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
|
||||||
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
|
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
|
||||||
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
|
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
|
||||||
|
"Auto Accept": "Auto Accept",
|
||||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
|
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
|
||||||
"Automatic upgrades": "Automatic upgrades",
|
"Automatic upgrades": "Automatic upgrades",
|
||||||
|
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
|
||||||
"Be careful!": "Be careful!",
|
"Be careful!": "Be careful!",
|
||||||
"Bugs": "Bugs",
|
"Bugs": "Bugs",
|
||||||
"CPU Utilization": "CPU Utilization",
|
"CPU Utilization": "CPU Utilization",
|
||||||
@ -43,12 +45,14 @@
|
|||||||
"Configured": "Configured",
|
"Configured": "Configured",
|
||||||
"Connection Error": "Connection Error",
|
"Connection Error": "Connection Error",
|
||||||
"Connection Type": "Connection Type",
|
"Connection Type": "Connection Type",
|
||||||
|
"Connections": "Connections",
|
||||||
"Copied from elsewhere": "Copied from elsewhere",
|
"Copied from elsewhere": "Copied from elsewhere",
|
||||||
"Copied from original": "Copied from original",
|
"Copied from original": "Copied from original",
|
||||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
||||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
|
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
|
||||||
"Danger!": "Danger!",
|
"Danger!": "Danger!",
|
||||||
|
"Default Folder Path": "Default Folder Path",
|
||||||
"Deleted": "Deleted",
|
"Deleted": "Deleted",
|
||||||
"Device": "Device",
|
"Device": "Device",
|
||||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||||
@ -101,6 +105,7 @@
|
|||||||
"GUI Listen Address": "GUI Listen Address",
|
"GUI Listen Address": "GUI Listen Address",
|
||||||
"GUI Listen Addresses": "GUI Listen Addresses",
|
"GUI Listen Addresses": "GUI Listen Addresses",
|
||||||
"GUI Theme": "GUI Theme",
|
"GUI Theme": "GUI Theme",
|
||||||
|
"General": "General",
|
||||||
"Generate": "Generate",
|
"Generate": "Generate",
|
||||||
"Global Changes": "Global Changes",
|
"Global Changes": "Global Changes",
|
||||||
"Global Discovery": "Global Discovery",
|
"Global Discovery": "Global Discovery",
|
||||||
@ -156,6 +161,7 @@
|
|||||||
"Override Changes": "Override Changes",
|
"Override Changes": "Override Changes",
|
||||||
"Path": "Path",
|
"Path": "Path",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||||
|
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).",
|
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
||||||
"Pause": "Pause",
|
"Pause": "Pause",
|
||||||
@ -264,6 +270,8 @@
|
|||||||
"Time": "Time",
|
"Time": "Time",
|
||||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||||
"Type": "Type",
|
"Type": "Type",
|
||||||
|
"Unavailable": "Unavailable",
|
||||||
|
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Unshared": "Unshared",
|
"Unshared": "Unshared",
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
<th><span class="fa fa-fw fa-share-alt"></span> <span translate>Shared With</span></th>
|
<th><span class="fa fa-fw fa-share-alt"></span> <span translate>Shared With</span></th>
|
||||||
<td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
|
<td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr ng-if="folderStats[folder.id].lastScan">
|
||||||
<th><span class="fa fa-fw fa-clock-o"></span> <span translate>Last Scan</span></th>
|
<th><span class="fa fa-fw fa-clock-o"></span> <span translate>Last Scan</span></th>
|
||||||
<td translate ng-if="folderStats[folder.id].lastScanDays >= 365" class="text-right">Never</td>
|
<td translate ng-if="folderStats[folder.id].lastScanDays >= 365" class="text-right">Never</td>
|
||||||
<td ng-if="folderStats[folder.id].lastScanDays < 365" class="text-right">
|
<td ng-if="folderStats[folder.id].lastScanDays < 365" class="text-right">
|
||||||
|
@ -24,31 +24,59 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
|
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label translate for="name">Device Name</label>
|
<div class="col-md-6">
|
||||||
<input id="name" class="form-control" type="text" ng-model="currentDevice.name" />
|
<div class="form-group">
|
||||||
<p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
|
<label translate for="name">Device Name</label>
|
||||||
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
|
<input id="name" class="form-control" type="text" ng-model="currentDevice.name" />
|
||||||
|
<p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
|
||||||
|
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="addresses">Addresses</label>
|
||||||
|
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||||
|
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label translate for="addresses">Addresses</label>
|
<div class="col-md-6">
|
||||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
<div class="form-group">
|
||||||
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
<label translate>Compression</label>
|
||||||
|
<select class="form-control" ng-model="currentDevice.compression">
|
||||||
|
<option value="always" translate>All Data</option>
|
||||||
|
<option value="metadata" translate>Metadata Only</option>
|
||||||
|
<option value="never" translate>Off</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label translate>Compression</label>
|
<div class="col-md-6">
|
||||||
<select class="form-control" ng-model="currentDevice.compression">
|
<div class="form-group">
|
||||||
<option value="always" translate>All Data</option>
|
<div class="checkbox">
|
||||||
<option value="metadata" translate>Metadata Only</option>
|
<label>
|
||||||
<option value="never" translate>Off</option>
|
<input type="checkbox" ng-model="currentDevice.introducer">
|
||||||
</select>
|
<span translate>Introducer</span>
|
||||||
</div>
|
<p translate class="help-block">Add devices from the introducer to our device list, for mutually shared folders.</p>
|
||||||
<div class="form-group">
|
</label>
|
||||||
<div class="checkbox">
|
</div>
|
||||||
<label>
|
</div>
|
||||||
<input type="checkbox" ng-model="currentDevice.introducer"> <span translate>Introducer</span>
|
</div>
|
||||||
</label>
|
<div class="col-md-6">
|
||||||
<p translate class="help-block">Add devices from the introducer to our device list, for mutually shared folders.</p>
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="currentDevice.autoAcceptFolders">
|
||||||
|
<span translate>Auto Accept</span>
|
||||||
|
<p translate class="help-block">Automatically create or share folders that this device advertises at the default path.</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -1,36 +1,180 @@
|
|||||||
<modal id="settings" status="default" icon="cog" heading="{{'Settings' | translate}}" large="yes" closeable="yes">
|
<modal id="settings" status="default" icon="cog" heading="{{'Settings' | translate}}" large="yes" closeable="yes">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form role="form" name="settingsEditor">
|
<form role="form" name="settingsEditor">
|
||||||
<div class="row">
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="active"><a data-toggle="tab" href="#settings-general" translate>General</a></li>
|
||||||
<div class="col-md-6">
|
<li><a data-toggle="tab" href="#settings-gui" translate>GUI</a></li>
|
||||||
|
<li><a data-toggle="tab" href="#settings-connections" translate>Connections</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div id="settings-general" class="tab-pane fade in active">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="DeviceName">Device Name</label>
|
<label translate for="DeviceName">Device Name</label>
|
||||||
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName"/>
|
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="form-group" ng-class="{'has-error': settingsEditor.minHomeDiskFree.$invalid && settingsEditor.minHomeDiskFree.$dirty}">
|
||||||
|
<label class="col-xs-12" for="minHomeDiskFree"><span translate>Minimum Free Disk Space</span></label><br/>
|
||||||
|
<div class="col-xs-9"><input name="minHomeDiskFree" id="minHomeDiskFree" class="form-control" type="number" ng-model="tmpOptions.minHomeDiskFree.value" required="" aria-required="true" min="0" step="0.01"/></div>
|
||||||
|
<div class="col-xs-3"><select class="col-sm-3 form-control" ng-model="tmpOptions.minHomeDiskFree.unit">
|
||||||
|
<option value="%">%</option>
|
||||||
|
<option value="kB">kB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
<option value="GB">GB</option>
|
||||||
|
<option value="TB">TB</option>
|
||||||
|
</select></div>
|
||||||
|
<p class="col-xs-12 help-block">
|
||||||
|
<span translate ng-show="settingsEditor.minHomeDiskFree.$invalid">Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size.</span>
|
||||||
|
<span translate ng-hide="settingsEditor.minHomeDiskFree.$invalid">This setting controls the free space required on the home (i.e., index database) disk.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate>API Key</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" readonly class="text-monospace form-control" value="{{tmpGUI.apiKey || '-'}}"/>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="button" class="btn btn-default btn-secondary" ng-click="setAPIKey(tmpGUI)">
|
||||||
|
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div ng-if="tmpOptions.upgrades != 'candidate'">
|
||||||
|
<label translate for="urVersion">Anonymous Usage Reporting</label> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||||
|
<select class="form-control" id="urVersion" ng-model="tmpOptions._urAcceptedStr">
|
||||||
|
<option ng-repeat="n in urVersions()" value="{{n}}">{{'Version' | translate}} {{n}}</option>
|
||||||
|
<!-- 1 does not exist, as we did not support incremental formats back then. -->
|
||||||
|
<option value="0" translate>Undecided (will prompt)</option>
|
||||||
|
<option value="-1" translate>Disabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="help-block" ng-if="tmpOptions.upgrades == 'candidate'">
|
||||||
|
<span translate>Usage reporting is always enabled for candidate releases.</span> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate>Automatic upgrades</label> <a href="https://docs.syncthing.net/users/releases.html" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
||||||
|
<select class="form-control" ng-model="tmpOptions.upgrades" ng-if="upgradeInfo">
|
||||||
|
<option value="none" translate>No upgrades</option>
|
||||||
|
<option value="stable" translate>Stable releases only</option>
|
||||||
|
<option value="candidate" translate>Stable releases and release candidates</option>
|
||||||
|
</select>
|
||||||
|
<p class="help-block" ng-if="!upgradeInfo">
|
||||||
|
<span translate>Unavailable/Disabled by administrator or maintainer</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="urVersion">Default Folder Path</label>
|
||||||
|
<input id="DefaultFolderPath" class="form-control" type="text" ng-model="tmpOptions.defaultFolderPath"/>
|
||||||
|
<p class="help-block">
|
||||||
|
<span translate translate-value-tilde="{{system.tilde}}">
|
||||||
|
Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-gui" class="tab-pane fade">
|
||||||
|
<div class="form-group" ng-class="{'has-error': settingsEditor.Address.$invalid && settingsEditor.Address.$dirty}">
|
||||||
|
<label translate for="Address">GUI Listen Address</label> <a href="https://docs.syncthing.net/users/guilisten.html" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
||||||
|
<input id="Address" name="Address" class="form-control" type="text" ng-model="tmpGUI.address" ng-pattern="/.*:0*((102[4-9])|(10[3-9][0-9])|(1[1-9][0-9][0-9])|([2-9][0-9][0-9][0-9])|([1-6]\d{4}))$/"/>
|
||||||
|
<p class="help-block" ng-show="settingsEditor.Address.$invalid" translate>
|
||||||
|
Enter a non-privileged port number (1024 - 65535).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="User">GUI Authentication User</label>
|
||||||
|
<input id="User" class="form-control" type="text" ng-model="tmpGUI.user"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="Password">GUI Authentication Password</label>
|
||||||
|
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.password" ng-trim="false"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS"/> <span translate>Use HTTPS for GUI</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser"/> <span translate>Start Browser</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate>GUI Theme</label>
|
||||||
|
<select class="form-control" ng-model="tmpGUI.theme" ng-if="themes.length > 1">
|
||||||
|
<option ng-repeat="theme in themes.sort()" value="{{ theme }}">
|
||||||
|
{{ themeName(theme) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="help-block" ng-if="themes.length < 2">
|
||||||
|
<span translate>Unavailable</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="settings-connections" class="tab-pane fade">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label> <a href="https://docs.syncthing.net/users/config.html#listen-addresses" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label> <a href="https://docs.syncthing.net/users/config.html#listen-addresses" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
||||||
|
|
||||||
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr"/>
|
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
|
<div class="col-md-6">
|
||||||
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
|
||||||
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0"/>
|
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
||||||
<p class="help-block">
|
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0"/>
|
||||||
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
<p class="help-block">
|
||||||
</p>
|
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
|
||||||
|
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
||||||
|
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0"/>
|
||||||
|
<p class="help-block">
|
||||||
|
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
|
|
||||||
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
|
||||||
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0"/>
|
|
||||||
<p class="help-block">
|
|
||||||
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -51,7 +195,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -72,106 +215,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div class="clearfix"></div>
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
|
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
|
||||||
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr"/>
|
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div class="form-horizontal">
|
<div class="col-md-6">
|
||||||
<div class="form-group" ng-class="{'has-error': settingsEditor.minHomeDiskFree.$invalid && settingsEditor.minHomeDiskFree.$dirty}">
|
|
||||||
<label class="col-xs-12" for="minHomeDiskFree"><span translate>Minimum Free Disk Space</span></label><br/>
|
|
||||||
<div class="col-xs-9"><input name="minHomeDiskFree" id="minHomeDiskFree" class="form-control" type="number" ng-model="tmpOptions.minHomeDiskFree.value" required="" aria-required="true" min="0" step="0.01"/></div>
|
|
||||||
<div class="col-xs-3"><select class="col-sm-3 form-control" ng-model="tmpOptions.minHomeDiskFree.unit">
|
|
||||||
<option value="%">%</option>
|
|
||||||
<option value="kB">kB</option>
|
|
||||||
<option value="MB">MB</option>
|
|
||||||
<option value="GB">GB</option>
|
|
||||||
<option value="TB">TB</option>
|
|
||||||
</select></div>
|
|
||||||
<p class="col-xs-12 help-block">
|
|
||||||
<span translate ng-show="settingsEditor.minHomeDiskFree.$invalid">Enter a non-negative number (e.g., "2.35") and select a unit. Percentages are as part of the total disk size.</span>
|
|
||||||
<span translate ng-hide="settingsEditor.minHomeDiskFree.$invalid">This setting controls the free space required on the home (i.e., index database) disk.</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group" ng-class="{'has-error': settingsEditor.Address.$invalid && settingsEditor.Address.$dirty}">
|
|
||||||
<label translate for="Address">GUI Listen Address</label> <a href="https://docs.syncthing.net/users/guilisten.html" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
|
||||||
<input id="Address" name="Address" class="form-control" type="text" ng-model="tmpGUI.address" ng-pattern="/.*:0*((102[4-9])|(10[3-9][0-9])|(1[1-9][0-9][0-9])|([2-9][0-9][0-9][0-9])|([1-6]\d{4}))$/"/>
|
|
||||||
<p class="help-block" ng-show="settingsEditor.Address.$invalid" translate>
|
|
||||||
Enter a non-privileged port number (1024 - 65535).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="User">GUI Authentication User</label>
|
|
||||||
<input id="User" class="form-control" type="text" ng-model="tmpGUI.user"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="Password">GUI Authentication Password</label>
|
|
||||||
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.password" ng-trim="false"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS"/> <span translate>Use HTTPS for GUI</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser"/> <span translate>Start Browser</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-if="upgradeInfo">
|
|
||||||
<label translate>Automatic upgrades</label> <a href="https://docs.syncthing.net/users/releases.html" target="_blank"><span class="fa fa-fw fa-book"></span> <span translate>Help</span></a>
|
|
||||||
<select class="form-control" ng-model="tmpOptions.upgrades">
|
|
||||||
<option value="none" translate>No upgrades</option>
|
|
||||||
<option value="stable" translate>Stable releases only</option>
|
|
||||||
<option value="candidate" translate>Stable releases and release candidates</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div ng-if="tmpOptions.upgrades != 'candidate'">
|
|
||||||
<label translate for="urVersion">Anonymous Usage Reporting</label> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
|
||||||
<select class="form-control" id="urVersion" ng-model="tmpOptions._urAcceptedStr">
|
|
||||||
<option ng-repeat="n in urVersions()" value="{{n}}">{{'Version' | translate}} {{n}}</option>
|
|
||||||
<!-- 1 does not exist, as we did not support incremental formats back then. -->
|
|
||||||
<option value="0" translate>Undecided (will prompt)</option>
|
|
||||||
<option value="-1" translate>Disabled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<p class="help-block" ng-if="tmpOptions.upgrades == 'candidate'">
|
|
||||||
<span translate>Usage reporting is always enabled for candidate releases.</span> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate>API Key</label>
|
|
||||||
<div class="well well-sm text-monospace" select-on-click>{{tmpGUI.apiKey || "-"}}</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">
|
|
||||||
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="themes.length > 1">
|
|
||||||
<label translate>GUI Theme</label>
|
|
||||||
<select class="form-control" ng-model="tmpGUI.theme">
|
|
||||||
<option ng-repeat="theme in themes.sort()" value="{{ theme }}">
|
|
||||||
{{ themeName(theme) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -52,7 +52,7 @@ func TestReplaceCommit(t *testing.T) {
|
|||||||
// Replace config. We should get back a clean response and the config
|
// Replace config. We should get back a clean response and the config
|
||||||
// should change.
|
// should change.
|
||||||
|
|
||||||
err := w.Replace(Configuration{Version: 1})
|
_, err := w.Replace(Configuration{Version: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Should not have a validation error:", err)
|
t.Fatal("Should not have a validation error:", err)
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func TestReplaceCommit(t *testing.T) {
|
|||||||
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
||||||
w.Subscribe(sub0)
|
w.Subscribe(sub0)
|
||||||
|
|
||||||
err = w.Replace(Configuration{Version: 2})
|
_, err = w.Replace(Configuration{Version: 2})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Should not have a validation error:", err)
|
t.Fatal("Should not have a validation error:", err)
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func TestReplaceCommit(t *testing.T) {
|
|||||||
|
|
||||||
w.Subscribe(validationError{})
|
w.Subscribe(validationError{})
|
||||||
|
|
||||||
err = w.Replace(Configuration{Version: 3})
|
_, err = w.Replace(Configuration{Version: 3})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Should have a validation error")
|
t.Fatal("Should have a validation error")
|
||||||
}
|
}
|
||||||
|
@ -826,7 +826,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
|
|||||||
t.Error("Should have less devices")
|
t.Error("Should have less devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = wrapper.Replace(raw)
|
_, err = wrapper.Replace(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed: %s", err)
|
t.Errorf("Failed: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ type DeviceConfiguration struct {
|
|||||||
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
|
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
|
||||||
Paused bool `xml:"paused" json:"paused"`
|
Paused bool `xml:"paused" json:"paused"`
|
||||||
AllowedNetworks []string `xml:"allowedNetwork,omitempty" json:"allowedNetworks"`
|
AllowedNetworks []string `xml:"allowedNetwork,omitempty" json:"allowedNetworks"`
|
||||||
|
AutoAcceptFolders bool `xml:"autoAcceptFolders" json:"autoAcceptFolders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {
|
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {
|
||||||
|
@ -24,7 +24,7 @@ var (
|
|||||||
const DefaultMarkerName = ".stfolder"
|
const DefaultMarkerName = ".stfolder"
|
||||||
|
|
||||||
type FolderConfiguration struct {
|
type FolderConfiguration struct {
|
||||||
ID string `xml:"id,attr" json:"id"`
|
ID string `xml:"id,attr" json:"id" restart:"false"`
|
||||||
Label string `xml:"label,attr" json:"label"`
|
Label string `xml:"label,attr" json:"label"`
|
||||||
FilesystemType fs.FilesystemType `xml:"filesystemType" json:"filesystemType"`
|
FilesystemType fs.FilesystemType `xml:"filesystemType" json:"filesystemType"`
|
||||||
Path string `xml:"path,attr" json:"path"`
|
Path string `xml:"path,attr" json:"path"`
|
||||||
@ -62,11 +62,18 @@ type FolderDeviceConfiguration struct {
|
|||||||
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
|
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFolderConfiguration(id string, fsType fs.FilesystemType, path string) FolderConfiguration {
|
func NewFolderConfiguration(myID protocol.DeviceID, id, label string, fsType fs.FilesystemType, path string) FolderConfiguration {
|
||||||
f := FolderConfiguration{
|
f := FolderConfiguration{
|
||||||
ID: id,
|
ID: id,
|
||||||
FilesystemType: fsType,
|
Label: label,
|
||||||
Path: path,
|
RescanIntervalS: 60,
|
||||||
|
FSWatcherDelayS: 10,
|
||||||
|
MinDiskFree: Size{Value: 1, Unit: "%"},
|
||||||
|
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
|
||||||
|
AutoNormalize: true,
|
||||||
|
MaxConflicts: -1,
|
||||||
|
FilesystemType: fsType,
|
||||||
|
Path: path,
|
||||||
}
|
}
|
||||||
f.prepare()
|
f.prepare()
|
||||||
return f
|
return f
|
||||||
|
@ -96,11 +96,11 @@ func (WeakHashSelectionMethod) ParseDefault(value string) (interface{}, error) {
|
|||||||
|
|
||||||
type OptionsConfiguration struct {
|
type OptionsConfiguration struct {
|
||||||
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
|
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
|
||||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default" restart:"true"`
|
||||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
|
||||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
|
||||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
|
||||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
|
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027" restart:"true"`
|
||||||
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
||||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||||
@ -117,21 +117,21 @@ type OptionsConfiguration struct {
|
|||||||
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||||
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
||||||
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
||||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true" restart:"true"`
|
||||||
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12" restart:"true"` // 0 for off
|
||||||
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases"` // when auto upgrades are enabled
|
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases" restart:"true"` // when auto upgrades are enabled
|
||||||
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
||||||
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false"`
|
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false" restart:"true"`
|
||||||
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
|
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
|
||||||
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
||||||
MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
|
MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
|
||||||
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json"`
|
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json" restart:"true"`
|
||||||
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||||
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
|
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
|
||||||
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
|
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
|
||||||
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
|
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
|
||||||
TrafficClass int `xml:"trafficClass" json:"trafficClass"`
|
TrafficClass int `xml:"trafficClass" json:"trafficClass"`
|
||||||
WeakHashSelectionMethod WeakHashSelectionMethod `xml:"weakHashSelectionMethod" json:"weakHashSelectionMethod"`
|
WeakHashSelectionMethod WeakHashSelectionMethod `xml:"weakHashSelectionMethod" json:"weakHashSelectionMethod" restart:"true"`
|
||||||
StunServers []string `xml:"stunServer" json:"stunServers" default:"default"`
|
StunServers []string `xml:"stunServer" json:"stunServers" default:"default"`
|
||||||
StunKeepaliveS int `xml:"stunKeepaliveSeconds" json:"stunKeepaliveSeconds" default:"24"`
|
StunKeepaliveS int `xml:"stunKeepaliveSeconds" json:"stunKeepaliveSeconds" default:"24"`
|
||||||
KCPNoDelay bool `xml:"kcpNoDelay" json:"kcpNoDelay" default:"false"`
|
KCPNoDelay bool `xml:"kcpNoDelay" json:"kcpNoDelay" default:"false"`
|
||||||
|
@ -45,6 +45,15 @@ type Committer interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Waiter allows to wait for the given config operation to complete.
|
||||||
|
type Waiter interface {
|
||||||
|
Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopWaiter struct{}
|
||||||
|
|
||||||
|
func (noopWaiter) Wait() {}
|
||||||
|
|
||||||
// A wrapper around a Configuration that manages loads, saves and published
|
// A wrapper around a Configuration that manages loads, saves and published
|
||||||
// notifications of changes to registered Handlers
|
// notifications of changes to registered Handlers
|
||||||
|
|
||||||
@ -130,37 +139,25 @@ func (w *Wrapper) RawCopy() Configuration {
|
|||||||
return w.cfg.Copy()
|
return w.cfg.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceBlocking swaps the current configuration object for the given one,
|
|
||||||
// and waits for subscribers to be notified.
|
|
||||||
func (w *Wrapper) ReplaceBlocking(cfg Configuration) error {
|
|
||||||
w.mut.Lock()
|
|
||||||
wg := sync.NewWaitGroup()
|
|
||||||
err := w.replaceLocked(cfg, wg)
|
|
||||||
w.mut.Unlock()
|
|
||||||
wg.Wait()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace swaps the current configuration object for the given one.
|
// Replace swaps the current configuration object for the given one.
|
||||||
func (w *Wrapper) Replace(cfg Configuration) error {
|
func (w *Wrapper) Replace(cfg Configuration) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
|
return w.replaceLocked(cfg)
|
||||||
return w.replaceLocked(cfg, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wrapper) replaceLocked(to Configuration, wg sync.WaitGroup) error {
|
func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
|
||||||
from := w.cfg
|
from := w.cfg
|
||||||
|
|
||||||
if err := to.clean(); err != nil {
|
if err := to.clean(); err != nil {
|
||||||
return err
|
return noopWaiter{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sub := range w.subs {
|
for _, sub := range w.subs {
|
||||||
l.Debugln(sub, "verifying configuration")
|
l.Debugln(sub, "verifying configuration")
|
||||||
if err := sub.VerifyConfiguration(from, to); err != nil {
|
if err := sub.VerifyConfiguration(from, to); err != nil {
|
||||||
l.Debugln(sub, "rejected config:", err)
|
l.Debugln(sub, "rejected config:", err)
|
||||||
return err
|
return noopWaiter{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,23 +165,19 @@ func (w *Wrapper) replaceLocked(to Configuration, wg sync.WaitGroup) error {
|
|||||||
w.deviceMap = nil
|
w.deviceMap = nil
|
||||||
w.folderMap = nil
|
w.folderMap = nil
|
||||||
|
|
||||||
w.notifyListeners(from, to, wg)
|
return w.notifyListeners(from, to), nil
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wrapper) notifyListeners(from, to Configuration, wg sync.WaitGroup) {
|
func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
|
||||||
if wg != nil {
|
wg := sync.NewWaitGroup()
|
||||||
wg.Add(len(w.subs))
|
wg.Add(len(w.subs))
|
||||||
}
|
|
||||||
for _, sub := range w.subs {
|
for _, sub := range w.subs {
|
||||||
go func(commiter Committer) {
|
go func(commiter Committer) {
|
||||||
w.notifyListener(commiter, from.Copy(), to.Copy())
|
w.notifyListener(commiter, from.Copy(), to.Copy())
|
||||||
if wg != nil {
|
wg.Done()
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
}(sub)
|
}(sub)
|
||||||
}
|
}
|
||||||
|
return wg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||||
@ -211,7 +204,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
|||||||
|
|
||||||
// SetDevices adds new devices to the configuration, or overwrites existing
|
// SetDevices adds new devices to the configuration, or overwrites existing
|
||||||
// devices with the same ID.
|
// devices with the same ID.
|
||||||
func (w *Wrapper) SetDevices(devs []DeviceConfiguration) error {
|
func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
|
|
||||||
@ -231,17 +224,17 @@ func (w *Wrapper) SetDevices(devs []DeviceConfiguration) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.replaceLocked(newCfg, nil)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||||
// device with the same ID.
|
// device with the same ID.
|
||||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
|
func (w *Wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
|
||||||
return w.SetDevices([]DeviceConfiguration{dev})
|
return w.SetDevices([]DeviceConfiguration{dev})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveDevice removes the device from the configuration
|
// RemoveDevice removes the device from the configuration
|
||||||
func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
|
func (w *Wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
|
|
||||||
@ -255,10 +248,10 @@ func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !removed {
|
if !removed {
|
||||||
return nil
|
return noopWaiter{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.replaceLocked(newCfg, nil)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Folders returns a map of folders. Folder structures should not be changed,
|
// Folders returns a map of folders. Folder structures should not be changed,
|
||||||
@ -277,7 +270,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
|||||||
|
|
||||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||||
// folder with the same ID.
|
// folder with the same ID.
|
||||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
|
func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
|
|
||||||
@ -294,7 +287,7 @@ func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
|
|||||||
newCfg.Folders = append(w.cfg.Folders, fld)
|
newCfg.Folders = append(w.cfg.Folders, fld)
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.replaceLocked(newCfg, nil)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options returns the current options configuration object.
|
// Options returns the current options configuration object.
|
||||||
@ -305,12 +298,12 @@ func (w *Wrapper) Options() OptionsConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetOptions replaces the current options configuration object.
|
// SetOptions replaces the current options configuration object.
|
||||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) error {
|
func (w *Wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
newCfg := w.cfg.Copy()
|
newCfg := w.cfg.Copy()
|
||||||
newCfg.Options = opts
|
newCfg.Options = opts
|
||||||
return w.replaceLocked(newCfg, nil)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GUI returns the current GUI configuration object.
|
// GUI returns the current GUI configuration object.
|
||||||
@ -321,12 +314,12 @@ func (w *Wrapper) GUI() GUIConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetGUI replaces the current GUI configuration object.
|
// SetGUI replaces the current GUI configuration object.
|
||||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) error {
|
func (w *Wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
|
||||||
w.mut.Lock()
|
w.mut.Lock()
|
||||||
defer w.mut.Unlock()
|
defer w.mut.Unlock()
|
||||||
newCfg := w.cfg.Copy()
|
newCfg := w.cfg.Copy()
|
||||||
newCfg.GUI = gui
|
newCfg.GUI = gui
|
||||||
return w.replaceLocked(newCfg, nil)
|
return w.replaceLocked(newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IgnoredDevice returns whether or not connection attempts from the given
|
// IgnoredDevice returns whether or not connection attempts from the given
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
stdsync "sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
@ -758,7 +759,7 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna
|
|||||||
for _, prio := range priorities {
|
for _, prio := range priorities {
|
||||||
tgts := dialTargetBuckets[prio]
|
tgts := dialTargetBuckets[prio]
|
||||||
res := make(chan internalConn, len(tgts))
|
res := make(chan internalConn, len(tgts))
|
||||||
wg := sync.NewWaitGroup()
|
wg := stdsync.WaitGroup{}
|
||||||
for _, tgt := range tgts {
|
for _, tgt := range tgts {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(tgt dialTarget) {
|
go func(tgt dialTarget) {
|
||||||
|
@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/stats"
|
"github.com/syncthing/syncthing/lib/stats"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
"github.com/syncthing/syncthing/lib/util"
|
||||||
"github.com/syncthing/syncthing/lib/versioner"
|
"github.com/syncthing/syncthing/lib/versioner"
|
||||||
"github.com/syncthing/syncthing/lib/weakhash"
|
"github.com/syncthing/syncthing/lib/weakhash"
|
||||||
"github.com/thejerf/suture"
|
"github.com/thejerf/suture"
|
||||||
@ -892,6 +893,8 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbLocation := filepath.Dir(m.db.Location())
|
dbLocation := filepath.Dir(m.db.Location())
|
||||||
|
changed := false
|
||||||
|
deviceCfg := m.cfg.Devices()[deviceID]
|
||||||
|
|
||||||
// See issue #3802 - in short, we can't send modern symlink entries to older
|
// See issue #3802 - in short, we can't send modern symlink entries to older
|
||||||
// clients.
|
// clients.
|
||||||
@ -901,6 +904,13 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
|||||||
dropSymlinks = true
|
dropSymlinks = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needs to happen outside of the fmut, as can cause CommitConfiguration
|
||||||
|
if deviceCfg.AutoAcceptFolders {
|
||||||
|
for _, folder := range cm.Folders {
|
||||||
|
changed = m.handleAutoAccepts(deviceCfg, folder) || changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.fmut.Lock()
|
m.fmut.Lock()
|
||||||
var paused []string
|
var paused []string
|
||||||
for _, folder := range cm.Folders {
|
for _, folder := range cm.Folders {
|
||||||
@ -927,6 +937,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
|||||||
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
|
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !folder.DisableTempIndexes {
|
if !folder.DisableTempIndexes {
|
||||||
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
||||||
}
|
}
|
||||||
@ -1021,8 +1032,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var changed = false
|
if deviceCfg.Introducer {
|
||||||
if deviceCfg := m.cfg.Devices()[deviceID]; deviceCfg.Introducer {
|
|
||||||
foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm)
|
foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm)
|
||||||
if introduced {
|
if introduced {
|
||||||
changed = true
|
changed = true
|
||||||
@ -1063,6 +1073,11 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
|
|||||||
// the folder.
|
// the folder.
|
||||||
nextDevice:
|
nextDevice:
|
||||||
for _, device := range folder.Devices {
|
for _, device := range folder.Devices {
|
||||||
|
// No need to share with self.
|
||||||
|
if device.ID == m.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
foldersDevices.set(device.ID, folder.ID)
|
foldersDevices.set(device.ID, folder.ID)
|
||||||
|
|
||||||
if _, ok := m.cfg.Devices()[device.ID]; !ok {
|
if _, ok := m.cfg.Devices()[device.ID]; !ok {
|
||||||
@ -1081,7 +1096,8 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
|
|||||||
|
|
||||||
// We don't yet share this folder with this device. Add the device
|
// We don't yet share this folder with this device. Add the device
|
||||||
// to sharing list of the folder.
|
// to sharing list of the folder.
|
||||||
m.introduceDeviceToFolder(device, folder, introducerCfg)
|
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
|
||||||
|
m.shareFolderWithDeviceLocked(device.ID, folder.ID, introducerCfg.DeviceID)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1089,7 +1105,7 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
|
|||||||
return foldersDevices, changed
|
return foldersDevices, changed
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleIntroductions handles removals of devices/shares that are removed by an introducer device
|
// handleDeintroductions handles removals of devices/shares that are removed by an introducer device
|
||||||
func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
|
func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
|
||||||
changed := false
|
changed := false
|
||||||
foldersIntroducedByOthers := make(folderDeviceSet)
|
foldersIntroducedByOthers := make(folderDeviceSet)
|
||||||
@ -1142,6 +1158,51 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
|
|||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleAutoAccepts handles adding and sharing folders for devices that have
|
||||||
|
// AutoAcceptFolders set to true.
|
||||||
|
func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
|
||||||
|
if _, ok := m.cfg.Folder(folder.ID); !ok {
|
||||||
|
defaultPath := m.cfg.Options().DefaultFolderPath
|
||||||
|
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
|
||||||
|
for _, path := range []string{folder.Label, folder.ID} {
|
||||||
|
if _, err := defaultPathFs.Lstat(path); !fs.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fcfg := config.NewFolderConfiguration(m.id, folder.ID, folder.Label, fs.FilesystemTypeBasic, filepath.Join(defaultPath, path))
|
||||||
|
|
||||||
|
// Need to wait for the waiter, as this calls CommitConfiguration,
|
||||||
|
// which sets up the folder and as we return from this call,
|
||||||
|
// ClusterConfig starts poking at m.folderFiles and other things
|
||||||
|
// that might not exist until the config is committed.
|
||||||
|
w, _ := m.cfg.SetFolder(fcfg)
|
||||||
|
w.Wait()
|
||||||
|
|
||||||
|
// This needs to happen under a lock.
|
||||||
|
m.fmut.Lock()
|
||||||
|
w = m.shareFolderWithDeviceLocked(deviceCfg.DeviceID, folder.ID, protocol.DeviceID{})
|
||||||
|
m.fmut.Unlock()
|
||||||
|
w.Wait()
|
||||||
|
l.Infof("Auto-accepted %s folder %s at path %s", deviceCfg.DeviceID, folder.Description(), fcfg.Path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceCfg.DeviceID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder already exists.
|
||||||
|
if !m.folderSharedWith(folder.ID, deviceCfg.DeviceID) {
|
||||||
|
m.fmut.Lock()
|
||||||
|
w := m.shareFolderWithDeviceLocked(deviceCfg.DeviceID, folder.ID, protocol.DeviceID{})
|
||||||
|
m.fmut.Unlock()
|
||||||
|
w.Wait()
|
||||||
|
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceCfg.DeviceID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
|
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
|
||||||
addresses := []string{"dynamic"}
|
addresses := []string{"dynamic"}
|
||||||
for _, addr := range device.Addresses {
|
for _, addr := range device.Addresses {
|
||||||
@ -1170,18 +1231,17 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
|
|||||||
m.cfg.SetDevice(newDeviceCfg)
|
m.cfg.SetDevice(newDeviceCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) introduceDeviceToFolder(device protocol.Device, folder protocol.Folder, introducerCfg config.DeviceConfiguration) {
|
func (m *Model) shareFolderWithDeviceLocked(deviceID protocol.DeviceID, folder string, introducer protocol.DeviceID) config.Waiter {
|
||||||
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
|
m.deviceFolders[deviceID] = append(m.deviceFolders[deviceID], folder)
|
||||||
|
m.folderDevices.set(deviceID, folder)
|
||||||
|
|
||||||
m.deviceFolders[device.ID] = append(m.deviceFolders[device.ID], folder.ID)
|
folderCfg := m.cfg.Folders()[folder]
|
||||||
m.folderDevices.set(device.ID, folder.ID)
|
|
||||||
|
|
||||||
folderCfg := m.cfg.Folders()[folder.ID]
|
|
||||||
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
|
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
|
||||||
DeviceID: device.ID,
|
DeviceID: deviceID,
|
||||||
IntroducedBy: introducerCfg.DeviceID,
|
IntroducedBy: introducer,
|
||||||
})
|
})
|
||||||
m.cfg.SetFolder(folderCfg)
|
w, _ := m.cfg.SetFolder(folderCfg)
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closed is called when a connection has been closed
|
// Closed is called when a connection has been closed
|
||||||
@ -1486,7 +1546,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
|||||||
conn.ClusterConfig(cm)
|
conn.ClusterConfig(cm)
|
||||||
|
|
||||||
device, ok := m.cfg.Devices()[deviceID]
|
device, ok := m.cfg.Devices()[deviceID]
|
||||||
if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) {
|
if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
|
||||||
device.Name = hello.DeviceName
|
device.Name = hello.DeviceName
|
||||||
m.cfg.SetDevice(device)
|
m.cfg.SetDevice(device)
|
||||||
m.cfg.Save()
|
m.cfg.Save()
|
||||||
@ -2366,10 +2426,10 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
if _, ok := fromFolders[folderID]; !ok {
|
if _, ok := fromFolders[folderID]; !ok {
|
||||||
// A folder was added.
|
// A folder was added.
|
||||||
if cfg.Paused {
|
if cfg.Paused {
|
||||||
l.Infoln(m, "Paused folder", cfg.Description())
|
l.Infoln("Paused folder", cfg.Description())
|
||||||
cfg.CreateRoot()
|
cfg.CreateRoot()
|
||||||
} else {
|
} else {
|
||||||
l.Infoln(m, "Adding folder", cfg.Description())
|
l.Infoln("Adding folder", cfg.Description())
|
||||||
m.AddFolder(cfg)
|
m.AddFolder(cfg)
|
||||||
m.StartFolder(folderID)
|
m.StartFolder(folderID)
|
||||||
}
|
}
|
||||||
@ -2388,8 +2448,12 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
// Check if anything differs, apart from the label.
|
// Check if anything differs, apart from the label.
|
||||||
toCfgCopy := toCfg
|
toCfgCopy := toCfg
|
||||||
fromCfgCopy := fromCfg
|
fromCfgCopy := fromCfg
|
||||||
fromCfgCopy.Label = ""
|
util.CopyMatchingTag(&toCfgCopy, &fromCfgCopy, "restart", func(v string) bool {
|
||||||
toCfgCopy.Label = ""
|
if len(v) > 0 && v != "false" {
|
||||||
|
panic(fmt.Sprintf(`unexpected struct value: %s. expected untagged or "false"`, v))
|
||||||
|
}
|
||||||
|
return v == "false"
|
||||||
|
})
|
||||||
|
|
||||||
if !reflect.DeepEqual(fromCfgCopy, toCfgCopy) {
|
if !reflect.DeepEqual(fromCfgCopy, toCfgCopy) {
|
||||||
m.RestartFolder(toCfg)
|
m.RestartFolder(toCfg)
|
||||||
@ -2432,17 +2496,15 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
|
|
||||||
// Some options don't require restart as those components handle it fine
|
// Some options don't require restart as those components handle it fine
|
||||||
// by themselves.
|
// by themselves.
|
||||||
from.Options.URAccepted = to.Options.URAccepted
|
|
||||||
from.Options.URSeen = to.Options.URSeen
|
// Copy fields that do not have the field set to true
|
||||||
from.Options.URUniqueID = to.Options.URUniqueID
|
util.CopyMatchingTag(&from.Options, &to.Options, "restart", func(v string) bool {
|
||||||
from.Options.ListenAddresses = to.Options.ListenAddresses
|
if len(v) > 0 && v != "true" {
|
||||||
from.Options.RelaysEnabled = to.Options.RelaysEnabled
|
panic(fmt.Sprintf(`unexpected struct value: %s. expected untagged or "true"`, v))
|
||||||
from.Options.UnackedNotificationIDs = to.Options.UnackedNotificationIDs
|
}
|
||||||
from.Options.MaxRecvKbps = to.Options.MaxRecvKbps
|
return v != "true"
|
||||||
from.Options.MaxSendKbps = to.Options.MaxSendKbps
|
})
|
||||||
from.Options.LimitBandwidthInLan = to.Options.LimitBandwidthInLan
|
|
||||||
from.Options.StunKeepaliveS = to.Options.StunKeepaliveS
|
|
||||||
from.Options.StunServers = to.Options.StunServers
|
|
||||||
// All of the other generic options require restart. Or at least they may;
|
// All of the other generic options require restart. Or at least they may;
|
||||||
// removing this check requires going through those options carefully and
|
// removing this check requires going through those options carefully and
|
||||||
// making sure there are individual services that handle them correctly.
|
// making sure there are individual services that handle them correctly.
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -36,13 +37,14 @@ var device1, device2 protocol.DeviceID
|
|||||||
var defaultConfig *config.Wrapper
|
var defaultConfig *config.Wrapper
|
||||||
var defaultFolderConfig config.FolderConfiguration
|
var defaultFolderConfig config.FolderConfiguration
|
||||||
var defaultFs fs.Filesystem
|
var defaultFs fs.Filesystem
|
||||||
|
var defaultAutoAcceptCfg config.Configuration
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
||||||
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
|
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
|
||||||
defaultFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
|
defaultFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
|
||||||
|
|
||||||
defaultFolderConfig = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "testdata")
|
defaultFolderConfig = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
|
||||||
defaultFolderConfig.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
|
defaultFolderConfig.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
|
||||||
_defaultConfig := config.Configuration{
|
_defaultConfig := config.Configuration{
|
||||||
Folders: []config.FolderConfiguration{defaultFolderConfig},
|
Folders: []config.FolderConfiguration{defaultFolderConfig},
|
||||||
@ -53,6 +55,17 @@ func init() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
defaultConfig = config.Wrap("/tmp/test", _defaultConfig)
|
defaultConfig = config.Wrap("/tmp/test", _defaultConfig)
|
||||||
|
defaultAutoAcceptCfg = config.Configuration{
|
||||||
|
Devices: []config.DeviceConfiguration{
|
||||||
|
{
|
||||||
|
DeviceID: device1,
|
||||||
|
AutoAcceptFolders: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Options: config.OptionsConfiguration{
|
||||||
|
DefaultFolderPath: "testdata",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var testDataExpected = map[string]protocol.FileInfo{
|
var testDataExpected = map[string]protocol.FileInfo{
|
||||||
@ -87,6 +100,20 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newState(cfg config.Configuration) (*config.Wrapper, *Model) {
|
||||||
|
db := db.OpenMemory()
|
||||||
|
|
||||||
|
wcfg := config.Wrap("/tmp/test", cfg)
|
||||||
|
|
||||||
|
m := NewModel(wcfg, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||||
|
for _, folder := range cfg.Folders {
|
||||||
|
m.AddFolder(folder)
|
||||||
|
}
|
||||||
|
m.ServeBackground()
|
||||||
|
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
|
||||||
|
return wcfg, m
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequest(t *testing.T) {
|
func TestRequest(t *testing.T) {
|
||||||
db := db.OpenMemory()
|
db := db.OpenMemory()
|
||||||
|
|
||||||
@ -609,20 +636,6 @@ func TestIntroducer(t *testing.T) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
newState := func(cfg config.Configuration) (*config.Wrapper, *Model) {
|
|
||||||
db := db.OpenMemory()
|
|
||||||
|
|
||||||
wcfg := config.Wrap("/tmp/test", cfg)
|
|
||||||
|
|
||||||
m := NewModel(wcfg, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
|
||||||
for _, folder := range cfg.Folders {
|
|
||||||
m.AddFolder(folder)
|
|
||||||
}
|
|
||||||
m.ServeBackground()
|
|
||||||
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
|
|
||||||
return wcfg, m
|
|
||||||
}
|
|
||||||
|
|
||||||
wcfg, m := newState(config.Configuration{
|
wcfg, m := newState(config.Configuration{
|
||||||
Devices: []config.DeviceConfiguration{
|
Devices: []config.DeviceConfiguration{
|
||||||
{
|
{
|
||||||
@ -970,6 +983,237 @@ func TestIntroducer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptRejected(t *testing.T) {
|
||||||
|
// Nothing happens if AutoAcceptFolders not set
|
||||||
|
tcfg := defaultAutoAcceptCfg.Copy()
|
||||||
|
tcfg.Devices[0].AutoAcceptFolders = false
|
||||||
|
wcfg, m := newState(tcfg)
|
||||||
|
id := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, ok := wcfg.Folder(id); ok || m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("unexpected shared", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptNewFolder(t *testing.T) {
|
||||||
|
// New folder
|
||||||
|
wcfg, m := newState(defaultAutoAcceptCfg)
|
||||||
|
id := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if _, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("expected shared", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptMultipleFolders(t *testing.T) {
|
||||||
|
// Multiple new folders
|
||||||
|
wcfg, m := newState(defaultAutoAcceptCfg)
|
||||||
|
id1 := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id1))
|
||||||
|
id2 := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id2))
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id1,
|
||||||
|
Label: id1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: id2,
|
||||||
|
Label: id2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if _, ok := wcfg.Folder(id1); !ok || !m.folderSharedWith(id1, device1) {
|
||||||
|
t.Error("expected shared", id1)
|
||||||
|
}
|
||||||
|
if _, ok := wcfg.Folder(id2); !ok || !m.folderSharedWith(id2, device1) {
|
||||||
|
t.Error("expected shared", id2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptExistingFolder(t *testing.T) {
|
||||||
|
// Existing folder
|
||||||
|
id := srand.String(8)
|
||||||
|
idOther := srand.String(8) // To check that path does not get changed.
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
|
||||||
|
tcfg := defaultAutoAcceptCfg.Copy()
|
||||||
|
tcfg.Folders = []config.FolderConfiguration{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Path: filepath.Join("testdata", idOther), // To check that path does not get changed.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wcfg, m := newState(tcfg)
|
||||||
|
if _, ok := wcfg.Folder(id); !ok || m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("missing folder, or shared", id)
|
||||||
|
}
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if fcfg, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) || fcfg.Path != filepath.Join("testdata", idOther) {
|
||||||
|
t.Error("missing folder, or unshared, or path changed", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptNewAndExistingFolder(t *testing.T) {
|
||||||
|
// New and existing folder
|
||||||
|
id1 := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id1))
|
||||||
|
id2 := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id2))
|
||||||
|
|
||||||
|
tcfg := defaultAutoAcceptCfg.Copy()
|
||||||
|
tcfg.Folders = []config.FolderConfiguration{
|
||||||
|
{
|
||||||
|
ID: id1,
|
||||||
|
Path: filepath.Join("testdata", id1), // from previous test case, to verify that path doesn't get changed.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wcfg, m := newState(tcfg)
|
||||||
|
if _, ok := wcfg.Folder(id1); !ok || m.folderSharedWith(id1, device1) {
|
||||||
|
t.Error("missing folder, or shared", id1)
|
||||||
|
}
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id1,
|
||||||
|
Label: id1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: id2,
|
||||||
|
Label: id2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, id := range []string{id1, id2} {
|
||||||
|
if _, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("missing folder, or unshared", i, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptAlreadyShared(t *testing.T) {
|
||||||
|
// Already shared
|
||||||
|
id := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
tcfg := defaultAutoAcceptCfg.Copy()
|
||||||
|
tcfg.Folders = []config.FolderConfiguration{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Path: filepath.Join("testdata", id),
|
||||||
|
Devices: []config.FolderDeviceConfiguration{
|
||||||
|
{
|
||||||
|
DeviceID: device1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wcfg, m := newState(tcfg)
|
||||||
|
if _, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("missing folder, or not shared", id)
|
||||||
|
}
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("missing folder, or not shared", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptNameConflict(t *testing.T) {
|
||||||
|
id := srand.String(8)
|
||||||
|
label := srand.String(8)
|
||||||
|
os.MkdirAll(filepath.Join("testdata", id), 0777)
|
||||||
|
os.MkdirAll(filepath.Join("testdata", label), 0777)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", label))
|
||||||
|
wcfg, m := newState(defaultAutoAcceptCfg)
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: label,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if _, ok := wcfg.Folder(id); ok || m.folderSharedWith(id, device1) {
|
||||||
|
t.Error("unexpected folder", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptPrefersLabel(t *testing.T) {
|
||||||
|
// Prefers label, falls back to ID.
|
||||||
|
wcfg, m := newState(defaultAutoAcceptCfg)
|
||||||
|
id := srand.String(8)
|
||||||
|
label := srand.String(8)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", label))
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: label,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if fcfg, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) || !strings.HasSuffix(fcfg.Path, label) {
|
||||||
|
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoAcceptFallsBackToID(t *testing.T) {
|
||||||
|
// Prefers label, falls back to ID.
|
||||||
|
wcfg, m := newState(defaultAutoAcceptCfg)
|
||||||
|
id := srand.String(8)
|
||||||
|
label := srand.String(8)
|
||||||
|
os.MkdirAll(filepath.Join("testdata", label), 0777)
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", label))
|
||||||
|
defer os.RemoveAll(filepath.Join("testdata", id))
|
||||||
|
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||||
|
Folders: []protocol.Folder{
|
||||||
|
{
|
||||||
|
ID: id,
|
||||||
|
Label: label,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if fcfg, ok := wcfg.Folder(id); !ok || !m.folderSharedWith(id, device1) || !strings.HasSuffix(fcfg.Path, id) {
|
||||||
|
t.Error("expected shared, or wrong path", id, label, fcfg.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func changeIgnores(t *testing.T, m *Model, expected []string) {
|
func changeIgnores(t *testing.T, m *Model, expected []string) {
|
||||||
arrEqual := func(a, b []string) bool {
|
arrEqual := func(a, b []string) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
@ -1920,7 +2164,9 @@ func TestIssue4357(t *testing.T) {
|
|||||||
defer m.Stop()
|
defer m.Stop()
|
||||||
|
|
||||||
// Force the model to wire itself and add the folders
|
// Force the model to wire itself and add the folders
|
||||||
if err := wrapper.ReplaceBlocking(cfg); err != nil {
|
p, err := wrapper.Replace(cfg)
|
||||||
|
p.Wait()
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1931,7 +2177,9 @@ func TestIssue4357(t *testing.T) {
|
|||||||
newCfg := wrapper.RawCopy()
|
newCfg := wrapper.RawCopy()
|
||||||
newCfg.Folders[0].Paused = true
|
newCfg.Folders[0].Paused = true
|
||||||
|
|
||||||
if err := wrapper.ReplaceBlocking(newCfg); err != nil {
|
p, err = wrapper.Replace(newCfg)
|
||||||
|
p.Wait()
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1943,7 +2191,9 @@ func TestIssue4357(t *testing.T) {
|
|||||||
t.Error("should still have folder in config")
|
t.Error("should still have folder in config")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := wrapper.ReplaceBlocking(config.Configuration{}); err != nil {
|
p, err = wrapper.Replace(config.Configuration{})
|
||||||
|
p.Wait()
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1952,7 +2202,9 @@ func TestIssue4357(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the folder back, should be running
|
// Add the folder back, should be running
|
||||||
if err := wrapper.ReplaceBlocking(cfg); err != nil {
|
p, err = wrapper.Replace(cfg)
|
||||||
|
p.Wait()
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1964,7 +2216,9 @@ func TestIssue4357(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should not panic when removing a running folder.
|
// Should not panic when removing a running folder.
|
||||||
if err := wrapper.ReplaceBlocking(config.Configuration{}); err != nil {
|
p, err = wrapper.Replace(config.Configuration{})
|
||||||
|
p.Wait()
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2066,7 +2320,7 @@ func TestIssue2782(t *testing.T) {
|
|||||||
|
|
||||||
db := db.OpenMemory()
|
db := db.OpenMemory()
|
||||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||||
m.AddFolder(config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "~/"+testName+"/synclink/"))
|
m.AddFolder(config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "~/"+testName+"/synclink/"))
|
||||||
m.StartFolder("default")
|
m.StartFolder("default")
|
||||||
m.ServeBackground()
|
m.ServeBackground()
|
||||||
defer m.Stop()
|
defer m.Stop()
|
||||||
@ -2111,7 +2365,7 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
|
|||||||
func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
||||||
dbi := db.OpenMemory()
|
dbi := db.OpenMemory()
|
||||||
|
|
||||||
fcfg := config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "testdata")
|
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
|
||||||
fcfg.Devices = []config.FolderDeviceConfiguration{
|
fcfg.Devices = []config.FolderDeviceConfiguration{
|
||||||
{DeviceID: device1},
|
{DeviceID: device1},
|
||||||
{DeviceID: device2},
|
{DeviceID: device2},
|
||||||
@ -2177,7 +2431,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
|||||||
cfg = cfg.Copy()
|
cfg = cfg.Copy()
|
||||||
cfg.Devices = cfg.Devices[:1]
|
cfg.Devices = cfg.Devices[:1]
|
||||||
|
|
||||||
if err := wcfg.Replace(cfg); err != nil {
|
if _, err := wcfg.Replace(cfg); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2350,7 +2604,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
|
|||||||
|
|
||||||
dbi := db.OpenMemory()
|
dbi := db.OpenMemory()
|
||||||
|
|
||||||
fcfg := config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "testdata")
|
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
|
||||||
fcfg.Devices = []config.FolderDeviceConfiguration{
|
fcfg.Devices = []config.FolderDeviceConfiguration{
|
||||||
{DeviceID: device1},
|
{DeviceID: device1},
|
||||||
{DeviceID: device2},
|
{DeviceID: device2},
|
||||||
|
@ -216,7 +216,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
|
|||||||
panic("Failed to create temporary testing dir")
|
panic("Failed to create temporary testing dir")
|
||||||
}
|
}
|
||||||
cfg := defaultConfig.RawCopy()
|
cfg := defaultConfig.RawCopy()
|
||||||
cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, tmpFolder)
|
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpFolder)
|
||||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||||
{DeviceID: device1},
|
{DeviceID: device1},
|
||||||
{DeviceID: device2},
|
{DeviceID: device2},
|
||||||
@ -292,7 +292,7 @@ func setupModelWithConnection() (*Model, *fakeConnection, string) {
|
|||||||
panic("Failed to create temporary testing dir")
|
panic("Failed to create temporary testing dir")
|
||||||
}
|
}
|
||||||
cfg := defaultConfig.RawCopy()
|
cfg := defaultConfig.RawCopy()
|
||||||
cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, tmpFolder)
|
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpFolder)
|
||||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||||
{DeviceID: device1},
|
{DeviceID: device1},
|
||||||
{DeviceID: device2},
|
{DeviceID: device2},
|
||||||
|
@ -7,9 +7,8 @@
|
|||||||
package nat
|
package nat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscoverFunc func(renewal, timeout time.Duration) []Device
|
type DiscoverFunc func(renewal, timeout time.Duration) []Device
|
||||||
@ -21,7 +20,7 @@ func Register(provider DiscoverFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoverAll(renewal, timeout time.Duration) map[string]Device {
|
func discoverAll(renewal, timeout time.Duration) map[string]Device {
|
||||||
wg := sync.NewWaitGroup()
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(providers))
|
wg.Add(len(providers))
|
||||||
|
|
||||||
c := make(chan Device)
|
c := make(chan Device)
|
||||||
|
@ -44,11 +44,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/dialer"
|
"github.com/syncthing/syncthing/lib/dialer"
|
||||||
"github.com/syncthing/syncthing/lib/nat"
|
"github.com/syncthing/syncthing/lib/nat"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -85,7 +85,7 @@ func Discover(renewal, timeout time.Duration) []nat.Device {
|
|||||||
|
|
||||||
resultChan := make(chan IGD)
|
resultChan := make(chan IGD)
|
||||||
|
|
||||||
wg := sync.NewWaitGroup()
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
for _, intf := range interfaces {
|
for _, intf := range interfaces {
|
||||||
// Interface flags seem to always be 0 on Windows
|
// Interface flags seem to always be 0 on Windows
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
@ -70,6 +71,31 @@ func SetDefaults(data interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyMatchingTag copies fields tagged tag:"value" from "from" struct onto "to" struct.
|
||||||
|
func CopyMatchingTag(from interface{}, to interface{}, tag string, shouldCopy func(value string) bool) {
|
||||||
|
fromStruct := reflect.ValueOf(from).Elem()
|
||||||
|
fromType := fromStruct.Type()
|
||||||
|
|
||||||
|
toStruct := reflect.ValueOf(to).Elem()
|
||||||
|
toType := toStruct.Type()
|
||||||
|
|
||||||
|
if fromType != toType {
|
||||||
|
panic(fmt.Sprintf("non equal types: %s != %s", fromType, toType))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < toStruct.NumField(); i++ {
|
||||||
|
fromField := fromStruct.Field(i)
|
||||||
|
toField := toStruct.Field(i)
|
||||||
|
|
||||||
|
structTag := toType.Field(i).Tag
|
||||||
|
|
||||||
|
v := structTag.Get(tag)
|
||||||
|
if shouldCopy(v) {
|
||||||
|
toField.Set(fromField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UniqueStrings returns a list on unique strings, trimming and sorting them
|
// UniqueStrings returns a list on unique strings, trimming and sorting them
|
||||||
// at the same time.
|
// at the same time.
|
||||||
func UniqueStrings(ss []string) []string {
|
func UniqueStrings(ss []string) []string {
|
||||||
|
@ -169,3 +169,61 @@ func TestAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCopyMatching(t *testing.T) {
|
||||||
|
type Nested struct {
|
||||||
|
A int
|
||||||
|
}
|
||||||
|
type Test struct {
|
||||||
|
CopyA int
|
||||||
|
CopyB []string
|
||||||
|
CopyC Nested
|
||||||
|
CopyD *Nested
|
||||||
|
NoCopy int `restart:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
from := Test{
|
||||||
|
CopyA: 1,
|
||||||
|
CopyB: []string{"friend", "foe"},
|
||||||
|
CopyC: Nested{
|
||||||
|
A: 2,
|
||||||
|
},
|
||||||
|
CopyD: &Nested{
|
||||||
|
A: 3,
|
||||||
|
},
|
||||||
|
NoCopy: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
to := Test{
|
||||||
|
CopyA: 11,
|
||||||
|
CopyB: []string{"foot", "toe"},
|
||||||
|
CopyC: Nested{
|
||||||
|
A: 22,
|
||||||
|
},
|
||||||
|
CopyD: &Nested{
|
||||||
|
A: 33,
|
||||||
|
},
|
||||||
|
NoCopy: 44,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy empty fields
|
||||||
|
CopyMatchingTag(&from, &to, "restart", func(v string) bool {
|
||||||
|
return v != "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
if to.CopyA != 1 {
|
||||||
|
t.Error("CopyA")
|
||||||
|
}
|
||||||
|
if len(to.CopyB) != 2 || to.CopyB[0] != "friend" || to.CopyB[1] != "foe" {
|
||||||
|
t.Error("CopyB")
|
||||||
|
}
|
||||||
|
if to.CopyC.A != 2 {
|
||||||
|
t.Error("CopyC")
|
||||||
|
}
|
||||||
|
if to.CopyD.A != 3 {
|
||||||
|
t.Error("CopyC")
|
||||||
|
}
|
||||||
|
if to.NoCopy != 44 {
|
||||||
|
t.Error("NoCopy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user