mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 10:58:57 +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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -367,3 +367,7 @@ ul.three-columns li, ul.two-columns li {
|
||||
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.",
|
||||
"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",
|
||||
|
@ -402,7 +402,7 @@
|
||||
<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>
|
||||
</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>
|
||||
<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">
|
||||
|
@ -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">
|
||||
|
@ -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> <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">
|
||||
<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"/>
|
||||
</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> <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>
|
||||
</form>
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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},
|
||||
|
@ -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},
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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