Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gui, lib/versioner: Allow to configure staggered versioning intervals (fixes #8352) #9094

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
14 changes: 14 additions & 0 deletions gui/default/assets/lang/lang-en.json
Expand Up @@ -200,10 +200,12 @@
"Ignored Devices": "Ignored Devices",
"Ignored Folders": "Ignored Folders",
"Ignored at": "Ignored at",
"In each interval, the first value means how often a version is kept, and the second value means a time period for when it happens.": "In each interval, the first value means how often a version is kept, and the second value means a time period for when it happens.",
"Included Software": "Included Software",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
"Internally used paths:": "Internally used paths:",
"Interval": "Interval",
"Introduced By": "Introduced By",
"Introducer": "Introducer",
"Introduction": "Introduction",
Expand Down Expand Up @@ -373,6 +375,7 @@
"Stable releases only": "Stable releases only",
"Staggered": "Staggered",
"Staggered File Versioning": "Staggered File Versioning",
"Staggered Intervals": "Staggered Intervals",
"Start Browser": "Start Browser",
"Statistics": "Statistics",
"Stopped": "Stopped",
Expand Down Expand Up @@ -411,19 +414,23 @@
"The device ID to enter here can be found in the \"Actions \u003e Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions \u003e Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.",
"The first value must be lower than the second value.": "The first value must be lower than the second value.",
"The folder ID cannot be blank.": "The folder ID cannot be blank.",
"The folder ID must be unique.": "The folder ID must be unique.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.",
"The folder path cannot be blank.": "The folder path cannot be blank.",
"The following intervals are used by default:": "The following intervals are used by default:",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items were changed locally.": "The following items were changed locally.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "The following methods are used to discover other devices on the network and announce this device to be found by others:",
"The following text will automatically be inserted into a new message.": "The following text will automatically be inserted into a new message.",
"The following unexpected items were found.": "The following unexpected items were found.",
"The interval editor is intended for advanced users only!": "The interval editor is intended for advanced users only!",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The last time period is tied to the maximum age and changes with it.": "The last time period is tied to the maximum age and changes with it.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
Expand All @@ -435,6 +442,7 @@
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The remote device has paused this folder.": "The remote device has paused this folder.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The second value must be higher than the first value. It must also be higher than the second value in the previous interval and lower than the second value in the next interval.": "The second value must be higher than the first value. It must also be higher than the second value in the previous interval and lower than the second value in the next interval.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
"There are no folders to share with this device.": "There are no folders to share with this device.",
Expand Down Expand Up @@ -504,6 +512,7 @@
"You can also copy and paste the text into a new message manually.": "You can also copy and paste the text into a new message manually.",
"You can also select one of these nearby devices:": "You can also select one of these nearby devices:",
"You can change your choice at any time in the Settings dialog.": "You can change your choice at any time in the Settings dialog.",
"You can customize the intervals below.": "You can customize the intervals below.",
"You can read more about the two release channels at the link below.": "You can read more about the two release channels at the link below.",
"You have no ignored devices.": "You have no ignored devices.",
"You have no ignored folders.": "You have no ignored folders.",
Expand All @@ -519,6 +528,10 @@
"file": "file",
"files": "files",
"folder": "folder",
"for the first 30 days a version is kept every day": "for the first 30 days a version is kept every day",
"for the first day a version is kept every hour": "for the first day a version is kept every hour",
"for the first hour a version is kept every 30 seconds": "for the first hour a version is kept every 30 seconds",
"for the first year a version is kept every week": "for the first year a version is kept every week",
"full documentation": "full documentation",
"items": "items",
"modified": "modified",
Expand All @@ -528,6 +541,7 @@
"theme-name-dark": "Dark",
"theme-name-default": "Default",
"theme-name-light": "Light",
"until the maximum age a version is kept every month": "until the maximum age a version is kept every month",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}}).",
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} might reintroduce this device."
Expand Down
8 changes: 6 additions & 2 deletions gui/default/index.html
Expand Up @@ -549,8 +549,12 @@ <h4 class="panel-title">
<span ng-if="folder.versioning.type == 'simple'" tooltip data-original-title="{{'Keep Versions' | translate}}">
&ensp;<span class="fa fa-file-archive-o"></span>&nbsp;{{folder.versioning.params.keep}}
</span>
<span ng-if="folder.versioning.type == 'staggered'" tooltip data-original-title="{{'Maximum Age' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.maxAge == 0" translate>Forever</span><span ng-if="folder.versioning.params.maxAge > 0">{{folder.versioning.params.maxAge | duration}}</span>
<span ng-if="folder.versioning.type == 'staggered'">
<span tooltip data-original-title="{{'Maximum Age' | translate}}">
&ensp;<span class="fa fa-calendar"></span>&nbsp;<span ng-if="folder.versioning.params.maxAge == 0" translate>Forever</span><span ng-if="folder.versioning.params.maxAge > 0">{{folder.versioning.params.maxAge | duration}}</span>
</span>
<span tooltip data-original-title="{{('Staggered Intervals' | translate) + ': ' + folder.versioning.params.staggeredInterval1 + '/' + folder.versioning.params.staggeredPeriod1 + ', ' + folder.versioning.params.staggeredInterval2 + '/' + folder.versioning.params.staggeredPeriod2 + ', ' + folder.versioning.params.staggeredInterval3 + '/' + folder.versioning.params.staggeredPeriod3 + ', ' + folder.versioning.params.staggeredInterval4 + '/' + folder.versioning.params.staggeredPeriod4 + ', ' + folder.versioning.params.staggeredInterval5 + '/' + (folder.versioning.params.maxAge === '0' ? ('Forever' | translate) : folder.versioning.params.maxAge)}}">
&ensp;<span class="fa fa-bars"></span>&nbsp;5</span>
</span>
<span tooltip data-original-title="{{'Cleanup Interval' | translate}}">
&ensp;<span class="fa fa-recycle"></span>&nbsp;<span ng-if="folder.versioning.cleanupIntervalS == 0" translate>Disabled</span><span ng-if="folder.versioning.cleanupIntervalS > 0">{{folder.versioning.cleanupIntervalS | duration}}</span>
Expand Down
80 changes: 79 additions & 1 deletion gui/default/syncthing/core/syncthingController.js
Expand Up @@ -73,10 +73,24 @@ angular.module('syncthing.core')
trashcanClean: 0,
cleanupIntervalS: 3600,
simpleKeep: 5,
staggeredMaxAge: 365,
staggeredInterval1: 30,
staggeredInterval2: 3600,
staggeredInterval3: 86400,
staggeredInterval4: 604800,
staggeredInterval5: 2592000,
staggeredPeriod1: 3600,
staggeredPeriod2: 86400,
staggeredPeriod3: 2592000,
staggeredPeriod4: 31536000,
staggeredMaxAge: 365, // 31536000 sec
externalCommand: "",
};

// The last staggered period is tied to the maximum age and cannot be
// modified on its own. It is ignored when versions are kept forever,
// and it is also not saved in the config file.
$scope.versioningDefaults.staggeredPeriod5 = $scope.versioningDefaults.staggeredMaxAge * 86400;

$scope.localStateTotal = {
bytes: 0,
directories: 0,
Expand Down Expand Up @@ -2161,6 +2175,16 @@ angular.module('syncthing.core')
$scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
break;
case "staggered":
$scope.currentFolder._guiVersioning.staggeredInterval1 = +currentVersioning.params.staggeredInterval1;
$scope.currentFolder._guiVersioning.staggeredInterval2 = +currentVersioning.params.staggeredInterval2;
$scope.currentFolder._guiVersioning.staggeredInterval3 = +currentVersioning.params.staggeredInterval3;
$scope.currentFolder._guiVersioning.staggeredInterval4 = +currentVersioning.params.staggeredInterval4;
$scope.currentFolder._guiVersioning.staggeredInterval5 = +currentVersioning.params.staggeredInterval5;
$scope.currentFolder._guiVersioning.staggeredPeriod1 = +currentVersioning.params.staggeredPeriod1;
$scope.currentFolder._guiVersioning.staggeredPeriod2 = +currentVersioning.params.staggeredPeriod2;
$scope.currentFolder._guiVersioning.staggeredPeriod3 = +currentVersioning.params.staggeredPeriod3;
$scope.currentFolder._guiVersioning.staggeredPeriod4 = +currentVersioning.params.staggeredPeriod4;
$scope.currentFolder._guiVersioning.staggeredPeriod5 = +currentVersioning.params.maxAge;
$scope.currentFolder._guiVersioning.staggeredMaxAge = Math.floor(+currentVersioning.params.maxAge / 86400);
break;
case "external":
Expand All @@ -2169,6 +2193,51 @@ angular.module('syncthing.core')
}
};

$scope.staggeredIntervalsState = function () {
// We needn't check period5 as it is always valid and equal to maxAge.
var interval1 = $scope.folderEditor.staggeredInterval1;
var interval2 = $scope.folderEditor.staggeredInterval2;
var interval3 = $scope.folderEditor.staggeredInterval3;
var interval4 = $scope.folderEditor.staggeredInterval4;
var interval5 = $scope.folderEditor.staggeredInterval5;
var period1 = $scope.folderEditor.staggeredPeriod1;
var period2 = $scope.folderEditor.staggeredPeriod2;
var period3 = $scope.folderEditor.staggeredPeriod3;
var period4 = $scope.folderEditor.staggeredPeriod4;

if (
(interval1.$dirty && interval1.$invalid)
|| (interval2.$dirty && interval2.$invalid)
|| (interval3.$dirty && interval3.$invalid)
|| (interval4.$dirty && interval4.$invalid)
|| (interval5.$dirty && interval5.$invalid)
|| (period1.$dirty && period1.$invalid)
|| (period2.$dirty && period2.$invalid)
|| (period3.$dirty && period3.$invalid)
|| (period4.$dirty && period4.$invalid)
) {
return 'invalid'
} else if (
(interval1.$dirty && interval1.$valid && (interval1.$modelValue >= period1.$modelValue))
|| (interval2.$dirty && interval2.$valid && (interval2.$modelValue >= period2.$modelValue))
|| (interval3.$dirty && interval3.$valid && (interval3.$modelValue >= period3.$modelValue))
|| (interval4.$dirty && interval4.$valid && (interval4.$modelValue >= period4.$modelValue))
|| (interval5.$dirty && interval5.$valid && (interval5.$modelValue >= period5.$modelValue))
) {
return 'intervalMaxError';
} else if (
(period1.$dirty && period1.$valid && (period1.$modelValue <= interval1.$modelValue || period1.$modelValue >= period2.$modelValue))
|| (period2.$dirty && period2.$valid && (period2.$modelValue <= interval2.$modelValue || period2.$modelValue >= period3.$modelValue || period2.$modelValue <= period1.$modelValue))
|| (period3.$dirty && period3.$valid && (period3.$modelValue <= interval3.$modelValue || period3.$modelValue >= period4.$modelValue || period3.$modelValue <= period2.$modelValue))
// Note: period4 need not be lower than period5 (maxAge).
|| (period4.$dirty && period4.$valid && (period4.$modelValue <= interval4.$modelValue || period4.$modelValue <= period3.$modelValue))
) {
return 'periodMinMaxError';
} else {
return 'valid';
}
};

$scope.editFolderExisting = function (folderCfg, initialTab) {
$scope.currentFolder = angular.copy(folderCfg);
$scope.currentFolder._editing = "existing";
Expand Down Expand Up @@ -2346,6 +2415,15 @@ angular.module('syncthing.core')
folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
break;
case "staggered":
folderCfg.versioning.params.staggeredInterval1 = '' + (folderCfg._guiVersioning.staggeredInterval1);
folderCfg.versioning.params.staggeredInterval2 = '' + (folderCfg._guiVersioning.staggeredInterval2);
folderCfg.versioning.params.staggeredInterval3 = '' + (folderCfg._guiVersioning.staggeredInterval3);
folderCfg.versioning.params.staggeredInterval4 = '' + (folderCfg._guiVersioning.staggeredInterval4);
folderCfg.versioning.params.staggeredInterval5 = '' + (folderCfg._guiVersioning.staggeredInterval5);
folderCfg.versioning.params.staggeredPeriod1 = '' + folderCfg._guiVersioning.staggeredPeriod1;
folderCfg.versioning.params.staggeredPeriod2 = '' + (folderCfg._guiVersioning.staggeredPeriod2);
folderCfg.versioning.params.staggeredPeriod3 = '' + (folderCfg._guiVersioning.staggeredPeriod3);
folderCfg.versioning.params.staggeredPeriod4 = '' + (folderCfg._guiVersioning.staggeredPeriod4);
folderCfg.versioning.params.maxAge = '' + (folderCfg._guiVersioning.staggeredMaxAge * 86400);
break;
case "external":
Expand Down