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:
Audrius Butkevicius 2017-12-07 07:08:24 +00:00 committed by Jakob Borg
parent c005b8dcb0
commit 445c4edeca
22 changed files with 771 additions and 284 deletions

View File

@ -112,12 +112,12 @@ type configIntf interface {
GUI() config.GUIConfiguration
RawCopy() config.Configuration
Options() config.OptionsConfiguration
Replace(cfg config.Configuration) error
Replace(cfg config.Configuration) (config.Waiter, error)
Subscribe(c config.Committer)
Folders() map[string]config.FolderConfiguration
Devices() map[protocol.DeviceID]config.DeviceConfiguration
SetDevice(config.DeviceConfiguration) error
SetDevices([]config.DeviceConfiguration) error
SetDevice(config.DeviceConfiguration) (config.Waiter, error)
SetDevices([]config.DeviceConfiguration) (config.Waiter, error)
Save() error
ListenAddresses() []string
RequiresRestart() bool
@ -809,7 +809,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
// Activate and save
if err := s.cfg.Replace(to); err != nil {
if _, err := s.cfg.Replace(to); err != nil {
l.Warnln("Replacing config:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -1201,7 +1201,7 @@ func (s *apiService) makeDevicePauseHandler(paused bool) http.HandlerFunc {
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)
}
}

View File

@ -1080,14 +1080,7 @@ func defaultConfig(cfgFile string) *config.Wrapper {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
defaultFolder = config.NewFolderConfiguration("default", 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
defaultFolder = config.NewFolderConfiguration(myID, "default", "Default Folder", fs.FilesystemTypeBasic, locations[locDefFolder])
} else {
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 {
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)
}
}

View File

@ -34,8 +34,8 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
return config.OptionsConfiguration{}
}
func (c *mockedConfig) Replace(cfg config.Configuration) error {
return nil
func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error) {
return nil, nil
}
func (c *mockedConfig) Subscribe(cm config.Committer) {}
@ -48,12 +48,12 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
return nil
}
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) error {
return nil
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
return nil, nil
}
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) error {
return nil
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) (config.Waiter, error) {
return nil, nil
}
func (c *mockedConfig) Save() error {

View File

@ -367,3 +367,7 @@ ul.three-columns li, ul.two-columns li {
width: 100%;
}
}
.tab-content {
padding-top: 10px;
}

View File

@ -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.",
"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}}?",
"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 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!",
"Bugs": "Bugs",
"CPU Utilization": "CPU Utilization",
@ -43,12 +45,14 @@
"Configured": "Configured",
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"Connections": "Connections",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 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}}.",
"Danger!": "Danger!",
"Default Folder Path": "Default Folder Path",
"Deleted": "Deleted",
"Device": "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 Addresses": "GUI Listen Addresses",
"GUI Theme": "GUI Theme",
"General": "General",
"Generate": "Generate",
"Global Changes": "Global Changes",
"Global Discovery": "Global Discovery",
@ -156,6 +161,7 @@
"Override Changes": "Override Changes",
"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 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 folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
"Pause": "Pause",
@ -264,6 +270,8 @@
"Time": "Time",
"Trash Can File Versioning": "Trash Can File Versioning",
"Type": "Type",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unknown": "Unknown",
"Unshared": "Unshared",

View File

@ -402,7 +402,7 @@
<th><span class="fa fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
<td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
</tr>
<tr>
<tr ng-if="folderStats[folder.id].lastScan">
<th><span class="fa fa-fw fa-clock-o"></span>&nbsp;<span translate>Last Scan</span></th>
<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">

View File

@ -24,31 +24,59 @@
</div>
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
</div>
<div class="form-group">
<label translate for="name">Device Name</label>
<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 class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="name">Device Name</label>
<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 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 class="row">
<div class="col-md-6">
<div class="form-group">
<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 class="form-group">
<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 class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentDevice.introducer"> <span translate>Introducer</span>
</label>
<p translate class="help-block">Add devices from the introducer to our device list, for mutually shared folders.</p>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentDevice.introducer">
<span translate>Introducer</span>
<p translate class="help-block">Add devices from the introducer to our device list, for mutually shared folders.</p>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<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 class="row">

View File

@ -1,36 +1,180 @@
<modal id="settings" status="default" icon="cog" heading="{{'Settings' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<form role="form" name="settingsEditor">
<div class="row">
<div class="col-md-6">
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#settings-general" translate>General</a></li>
<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">
<label translate for="DeviceName">Device Name</label>
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName"/>
</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>&nbsp;<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>&emsp;<a href="https://docs.syncthing.net/users/releases.html" target="_blank"><span class="fa fa-fw fa-book"></span>&nbsp;<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>&emsp;<a href="https://docs.syncthing.net/users/guilisten.html" target="_blank"><span class="fa fa-fw fa-book"></span>&nbsp;<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">
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>&emsp;<a href="https://docs.syncthing.net/users/config.html#listen-addresses" target="_blank"><span class="fa fa-fw fa-book"></span>&nbsp;<span translate>Help</span></a>
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr"/>
</div>
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0"/>
<p class="help-block">
<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 class="row">
<div class="col-md-6">
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0"/>
<p class="help-block">
<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 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="col-md-6">
<div class="form-group">
@ -51,7 +195,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
@ -72,106 +215,16 @@
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr"/>
</div>
<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 class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr"/>
</div>
<div>
<div class="col-md-6">
</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>&emsp;<a href="https://docs.syncthing.net/users/guilisten.html" target="_blank"><span class="fa fa-fw fa-book"></span>&nbsp;<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>&emsp;<a href="https://docs.syncthing.net/users/releases.html" target="_blank"><span class="fa fa-fw fa-book"></span>&nbsp;<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>&nbsp;<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>
</form>

View File

@ -52,7 +52,7 @@ func TestReplaceCommit(t *testing.T) {
// Replace config. We should get back a clean response and the config
// should change.
err := w.Replace(Configuration{Version: 1})
_, err := w.Replace(Configuration{Version: 1})
if err != nil {
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)}
w.Subscribe(sub0)
err = w.Replace(Configuration{Version: 2})
_, err = w.Replace(Configuration{Version: 2})
if err != nil {
t.Fatal("Should not have a validation error:", err)
}
@ -87,7 +87,7 @@ func TestReplaceCommit(t *testing.T) {
w.Subscribe(validationError{})
err = w.Replace(Configuration{Version: 3})
_, err = w.Replace(Configuration{Version: 3})
if err == nil {
t.Fatal("Should have a validation error")
}

View File

@ -826,7 +826,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Error("Should have less devices")
}
err = wrapper.Replace(raw)
_, err = wrapper.Replace(raw)
if err != nil {
t.Errorf("Failed: %s", err)
}

View File

@ -19,6 +19,7 @@ type DeviceConfiguration struct {
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
Paused bool `xml:"paused" json:"paused"`
AllowedNetworks []string `xml:"allowedNetwork,omitempty" json:"allowedNetworks"`
AutoAcceptFolders bool `xml:"autoAcceptFolders" json:"autoAcceptFolders"`
}
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {

View File

@ -24,7 +24,7 @@ var (
const DefaultMarkerName = ".stfolder"
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"`
FilesystemType fs.FilesystemType `xml:"filesystemType" json:"filesystemType"`
Path string `xml:"path,attr" json:"path"`
@ -62,11 +62,18 @@ type FolderDeviceConfiguration struct {
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{
ID: id,
FilesystemType: fsType,
Path: path,
ID: id,
Label: label,
RescanIntervalS: 60,
FSWatcherDelayS: 10,
MinDiskFree: Size{Value: 1, Unit: "%"},
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
AutoNormalize: true,
MaxConflicts: -1,
FilesystemType: fsType,
Path: path,
}
f.prepare()
return f

View File

@ -96,11 +96,11 @@ func (WeakHashSelectionMethod) ParseDefault(value string) (interface{}, error) {
type OptionsConfiguration struct {
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default" restart:"true"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027" restart:"true"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
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"`
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true" restart:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12" restart:"true"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases" restart:"true"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false" restart:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
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"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
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"`
StunKeepaliveS int `xml:"stunKeepaliveSeconds" json:"stunKeepaliveSeconds" default:"24"`
KCPNoDelay bool `xml:"kcpNoDelay" json:"kcpNoDelay" default:"false"`

View File

@ -45,6 +45,15 @@ type Committer interface {
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
// notifications of changes to registered Handlers
@ -130,37 +139,25 @@ func (w *Wrapper) RawCopy() Configuration {
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.
func (w *Wrapper) Replace(cfg Configuration) error {
func (w *Wrapper) Replace(cfg Configuration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
return w.replaceLocked(cfg, nil)
return w.replaceLocked(cfg)
}
func (w *Wrapper) replaceLocked(to Configuration, wg sync.WaitGroup) error {
func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
from := w.cfg
if err := to.clean(); err != nil {
return err
return noopWaiter{}, err
}
for _, sub := range w.subs {
l.Debugln(sub, "verifying configuration")
if err := sub.VerifyConfiguration(from, to); err != nil {
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.folderMap = nil
w.notifyListeners(from, to, wg)
return nil
return w.notifyListeners(from, to), nil
}
func (w *Wrapper) notifyListeners(from, to Configuration, wg sync.WaitGroup) {
if wg != nil {
wg.Add(len(w.subs))
}
func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
wg := sync.NewWaitGroup()
wg.Add(len(w.subs))
for _, sub := range w.subs {
go func(commiter Committer) {
w.notifyListener(commiter, from.Copy(), to.Copy())
if wg != nil {
wg.Done()
}
wg.Done()
}(sub)
}
return wg
}
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
// devices with the same ID.
func (w *Wrapper) SetDevices(devs []DeviceConfiguration) error {
func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
w.mut.Lock()
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
// 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})
}
// 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()
defer w.mut.Unlock()
@ -255,10 +248,10 @@ func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
}
}
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,
@ -277,7 +270,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
@ -294,7 +287,7 @@ func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
newCfg.Folders = append(w.cfg.Folders, fld)
}
return w.replaceLocked(newCfg, nil)
return w.replaceLocked(newCfg)
}
// Options returns the current options configuration object.
@ -305,12 +298,12 @@ func (w *Wrapper) Options() OptionsConfiguration {
}
// 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()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.Options = opts
return w.replaceLocked(newCfg, nil)
return w.replaceLocked(newCfg)
}
// GUI returns the current GUI configuration object.
@ -321,12 +314,12 @@ func (w *Wrapper) GUI() GUIConfiguration {
}
// 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()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.GUI = gui
return w.replaceLocked(newCfg, nil)
return w.replaceLocked(newCfg)
}
// IgnoredDevice returns whether or not connection attempts from the given

View File

@ -14,6 +14,7 @@ import (
"net/url"
"sort"
"strings"
stdsync "sync"
"time"
"github.com/syncthing/syncthing/lib/config"
@ -758,7 +759,7 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna
for _, prio := range priorities {
tgts := dialTargetBuckets[prio]
res := make(chan internalConn, len(tgts))
wg := sync.NewWaitGroup()
wg := stdsync.WaitGroup{}
for _, tgt := range tgts {
wg.Add(1)
go func(tgt dialTarget) {

View File

@ -33,6 +33,7 @@ import (
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/syncthing/syncthing/lib/versioner"
"github.com/syncthing/syncthing/lib/weakhash"
"github.com/thejerf/suture"
@ -892,6 +893,8 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
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
// clients.
@ -901,6 +904,13 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
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()
var paused []string
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)
continue
}
if !folder.DisableTempIndexes {
tempIndexFolders = append(tempIndexFolders, folder.ID)
}
@ -1021,8 +1032,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
}
var changed = false
if deviceCfg := m.cfg.Devices()[deviceID]; deviceCfg.Introducer {
if deviceCfg.Introducer {
foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm)
if introduced {
changed = true
@ -1063,6 +1073,11 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
// the folder.
nextDevice:
for _, device := range folder.Devices {
// No need to share with self.
if device.ID == m.id {
continue
}
foldersDevices.set(device.ID, folder.ID)
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
// 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
}
}
@ -1089,7 +1105,7 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
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 {
changed := false
foldersIntroducedByOthers := make(folderDeviceSet)
@ -1142,6 +1158,51 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
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) {
addresses := []string{"dynamic"}
for _, addr := range device.Addresses {
@ -1170,18 +1231,17 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
m.cfg.SetDevice(newDeviceCfg)
}
func (m *Model) introduceDeviceToFolder(device protocol.Device, folder protocol.Folder, introducerCfg config.DeviceConfiguration) {
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
func (m *Model) shareFolderWithDeviceLocked(deviceID protocol.DeviceID, folder string, introducer protocol.DeviceID) config.Waiter {
m.deviceFolders[deviceID] = append(m.deviceFolders[deviceID], folder)
m.folderDevices.set(deviceID, folder)
m.deviceFolders[device.ID] = append(m.deviceFolders[device.ID], folder.ID)
m.folderDevices.set(device.ID, folder.ID)
folderCfg := m.cfg.Folders()[folder.ID]
folderCfg := m.cfg.Folders()[folder]
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
DeviceID: device.ID,
IntroducedBy: introducerCfg.DeviceID,
DeviceID: deviceID,
IntroducedBy: introducer,
})
m.cfg.SetFolder(folderCfg)
w, _ := m.cfg.SetFolder(folderCfg)
return w
}
// 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)
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
m.cfg.SetDevice(device)
m.cfg.Save()
@ -2366,10 +2426,10 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
if _, ok := fromFolders[folderID]; !ok {
// A folder was added.
if cfg.Paused {
l.Infoln(m, "Paused folder", cfg.Description())
l.Infoln("Paused folder", cfg.Description())
cfg.CreateRoot()
} else {
l.Infoln(m, "Adding folder", cfg.Description())
l.Infoln("Adding folder", cfg.Description())
m.AddFolder(cfg)
m.StartFolder(folderID)
}
@ -2388,8 +2448,12 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
// Check if anything differs, apart from the label.
toCfgCopy := toCfg
fromCfgCopy := fromCfg
fromCfgCopy.Label = ""
toCfgCopy.Label = ""
util.CopyMatchingTag(&toCfgCopy, &fromCfgCopy, "restart", func(v string) bool {
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) {
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
// by themselves.
from.Options.URAccepted = to.Options.URAccepted
from.Options.URSeen = to.Options.URSeen
from.Options.URUniqueID = to.Options.URUniqueID
from.Options.ListenAddresses = to.Options.ListenAddresses
from.Options.RelaysEnabled = to.Options.RelaysEnabled
from.Options.UnackedNotificationIDs = to.Options.UnackedNotificationIDs
from.Options.MaxRecvKbps = to.Options.MaxRecvKbps
from.Options.MaxSendKbps = to.Options.MaxSendKbps
from.Options.LimitBandwidthInLan = to.Options.LimitBandwidthInLan
from.Options.StunKeepaliveS = to.Options.StunKeepaliveS
from.Options.StunServers = to.Options.StunServers
// Copy fields that do not have the field set to true
util.CopyMatchingTag(&from.Options, &to.Options, "restart", func(v string) bool {
if len(v) > 0 && v != "true" {
panic(fmt.Sprintf(`unexpected struct value: %s. expected untagged or "true"`, v))
}
return v != "true"
})
// All of the other generic options require restart. Or at least they may;
// removing this check requires going through those options carefully and
// making sure there are individual services that handle them correctly.

View File

@ -18,6 +18,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"
@ -36,13 +37,14 @@ var device1, device2 protocol.DeviceID
var defaultConfig *config.Wrapper
var defaultFolderConfig config.FolderConfiguration
var defaultFs fs.Filesystem
var defaultAutoAcceptCfg config.Configuration
func init() {
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
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}}
_defaultConfig := config.Configuration{
Folders: []config.FolderConfiguration{defaultFolderConfig},
@ -53,6 +55,17 @@ func init() {
},
}
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{
@ -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) {
db := db.OpenMemory()
@ -609,20 +636,6 @@ func TestIntroducer(t *testing.T) {
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{
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) {
arrEqual := func(a, b []string) bool {
if len(a) != len(b) {
@ -1920,7 +2164,9 @@ func TestIssue4357(t *testing.T) {
defer m.Stop()
// 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)
}
@ -1931,7 +2177,9 @@ func TestIssue4357(t *testing.T) {
newCfg := wrapper.RawCopy()
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)
}
@ -1943,7 +2191,9 @@ func TestIssue4357(t *testing.T) {
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)
}
@ -1952,7 +2202,9 @@ func TestIssue4357(t *testing.T) {
}
// 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)
}
@ -1964,7 +2216,9 @@ func TestIssue4357(t *testing.T) {
}
// 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)
}
@ -2066,7 +2320,7 @@ func TestIssue2782(t *testing.T) {
db := db.OpenMemory()
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.ServeBackground()
defer m.Stop()
@ -2111,7 +2365,7 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
func TestSharedWithClearedOnDisconnect(t *testing.T) {
dbi := db.OpenMemory()
fcfg := config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "testdata")
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
fcfg.Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
@ -2177,7 +2431,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
cfg = cfg.Copy()
cfg.Devices = cfg.Devices[:1]
if err := wcfg.Replace(cfg); err != nil {
if _, err := wcfg.Replace(cfg); err != nil {
t.Error(err)
}
@ -2350,7 +2604,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
dbi := db.OpenMemory()
fcfg := config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "testdata")
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
fcfg.Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},

View File

@ -216,7 +216,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
panic("Failed to create temporary testing dir")
}
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{
{DeviceID: device1},
{DeviceID: device2},
@ -292,7 +292,7 @@ func setupModelWithConnection() (*Model, *fakeConnection, string) {
panic("Failed to create temporary testing dir")
}
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{
{DeviceID: device1},
{DeviceID: device2},

View File

@ -7,9 +7,8 @@
package nat
import (
"sync"
"time"
"github.com/syncthing/syncthing/lib/sync"
)
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 {
wg := sync.NewWaitGroup()
wg := &sync.WaitGroup{}
wg.Add(len(providers))
c := make(chan Device)

View File

@ -44,11 +44,11 @@ import (
"net/url"
"runtime"
"strings"
"sync"
"time"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/sync"
)
func init() {
@ -85,7 +85,7 @@ func Discover(renewal, timeout time.Duration) []nat.Device {
resultChan := make(chan IGD)
wg := sync.NewWaitGroup()
wg := &sync.WaitGroup{}
for _, intf := range interfaces {
// Interface flags seem to always be 0 on Windows

View File

@ -7,6 +7,7 @@
package util
import (
"fmt"
"net/url"
"reflect"
"sort"
@ -70,6 +71,31 @@ func SetDefaults(data interface{}) error {
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
// at the same time.
func UniqueStrings(ss []string) []string {

View File

@ -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")
}
}