diff --git a/gui/default/assets/css/overrides.css b/gui/default/assets/css/overrides.css index 62abbbec668..1ebb70788e8 100644 --- a/gui/default/assets/css/overrides.css +++ b/gui/default/assets/css/overrides.css @@ -521,6 +521,12 @@ ul.three-columns li, ul.two-columns li { opacity: 1; } +/* If the table has borders already, there is no need for additional borders + on input elements inside it. */ +.table-bordered .form-control { + border: 0; +} + .checkbox[disabled] { background-color: #eeeeee; opacity: 1; @@ -539,6 +545,12 @@ ul.three-columns li, ul.two-columns li { padding-bottom: 6px; } +/* Bootstrap changes both the text color and background color to red, making + the text unreadable, so let's restore the original text color. */ +.has-error .input-group-addon { + color: #555555; +} + /* CJK languages don't use italic at all, hence don't force it on them. */ html[lang|="zh"] i, html[lang="ja"] i, diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json index 12e48fc5ba0..32fb58da086 100644 --- a/gui/default/assets/lang/lang-en.json +++ b/gui/default/assets/lang/lang-en.json @@ -201,10 +201,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", @@ -375,6 +377,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", @@ -413,19 +416,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 connections must be a non-negative number.": "The number of connections must be a non-negative number.", @@ -439,6 +446,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.", @@ -509,6 +517,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.", @@ -524,8 +533,14 @@ "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", + "hours": "hours", "items": "items", + "minutes": "minutes", "modified": "modified", "permit": "permit", "seconds": "seconds", @@ -533,6 +548,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." diff --git a/gui/default/index.html b/gui/default/index.html index 5b151cb0026..42fa6e6443a 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -549,8 +549,12 @@

 {{folder.versioning.params.keep}} - -   Forever{{folder.versioning.params.maxAge | duration}} + + +   Forever{{folder.versioning.params.maxAge | duration}} + + +   {{countEnabledStaggeredIntervals(folder)}}  Disabled{{folder.versioning.cleanupIntervalS | duration}} diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 7c098aa619b..b4e03907ab4 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -73,7 +73,16 @@ angular.module('syncthing.core') trashcanClean: 0, cleanupIntervalS: 3600, simpleKeep: 5, - staggeredMaxAge: 365, + staggeredInterval1: 30, // seconds + staggeredInterval2: 1, // hour = 3600 seconds + staggeredInterval3: 1, // day = 86400 seconds + staggeredInterval4: 7, // days = 604800 seconds + staggeredInterval5: 30, // days = 2592000 seconds + staggeredPeriod1: 60, // minutes = 3600 seconds + staggeredPeriod2: 24, // hours = 86400 seconds + staggeredPeriod3: 30, // days = 2592000 seconds + staggeredPeriod4: 365, // year = 31536000 seconds + staggeredMaxAge: 365, // year = 31536000 seconds externalCommand: "", }; @@ -2166,6 +2175,15 @@ 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 = Math.floor(+currentVersioning.params.staggeredInterval2 / 3600); + $scope.currentFolder._guiVersioning.staggeredInterval3 = Math.floor(+currentVersioning.params.staggeredInterval3 / 86400); + $scope.currentFolder._guiVersioning.staggeredInterval4 = Math.floor(+currentVersioning.params.staggeredInterval4 / 86400); + $scope.currentFolder._guiVersioning.staggeredInterval5 = Math.floor(+currentVersioning.params.staggeredInterval5 / 86400); + $scope.currentFolder._guiVersioning.staggeredPeriod1 = Math.floor(+currentVersioning.params.staggeredPeriod1 / 60); + $scope.currentFolder._guiVersioning.staggeredPeriod2 = Math.floor(+currentVersioning.params.staggeredPeriod2 / 3600); + $scope.currentFolder._guiVersioning.staggeredPeriod3 = Math.floor(+currentVersioning.params.staggeredPeriod3 / 86400); + $scope.currentFolder._guiVersioning.staggeredPeriod4 = Math.floor(+currentVersioning.params.staggeredPeriod4 / 86400); $scope.currentFolder._guiVersioning.staggeredMaxAge = Math.floor(+currentVersioning.params.maxAge / 86400); break; case "external": @@ -2174,6 +2192,111 @@ angular.module('syncthing.core') } }; + $scope.areStaggeredIntervalsValid = function () { + if ( + ($scope.folderEditor.staggeredInterval1.$dirty && $scope.folderEditor.staggeredInterval1.$invalid) + || ($scope.folderEditor.staggeredInterval2.$dirty && $scope.folderEditor.staggeredInterval2.$invalid) + || ($scope.folderEditor.staggeredInterval3.$dirty && $scope.folderEditor.staggeredInterval3.$invalid) + || ($scope.folderEditor.staggeredInterval4.$dirty && $scope.folderEditor.staggeredInterval4.$invalid) + || ($scope.folderEditor.staggeredInterval5.$dirty && $scope.folderEditor.staggeredInterval5.$invalid) + || ($scope.folderEditor.staggeredPeriod1.$dirty && $scope.folderEditor.staggeredPeriod1.$invalid) + || ($scope.folderEditor.staggeredPeriod2.$dirty && $scope.folderEditor.staggeredPeriod2.$invalid) + || ($scope.folderEditor.staggeredPeriod3.$dirty && $scope.folderEditor.staggeredPeriod3.$invalid) + || ($scope.folderEditor.staggeredPeriod4.$dirty && $scope.folderEditor.staggeredPeriod4.$invalid) + ) { + return false; + } else { + return true; + } + }; + + $scope.isStaggeredIntervalDisabled = function (args, folder) { + // period5 must be defined separately as the name is staggeredMaxAge + // in the GUI and just maxAge in the config. + var period5 = ''; + if (folder) { + folder = folder.versioning.params; + period5 = folder.maxAge * 86400; + } else { + folder = $scope.currentFolder._guiVersioning; + period5 = folder.staggeredMaxAge * 86400; + } + var interval1 = folder.staggeredInterval1; + var interval2 = folder.staggeredInterval2 * 3600; + var interval3 = folder.staggeredInterval3 * 86400; + var interval4 = folder.staggeredInterval4 * 86400; + var interval5 = folder.staggeredInterval5 * 86400; + var period1 = folder.staggeredPeriod1 * 60; + var period2 = folder.staggeredPeriod2 * 3600; + var period3 = folder.staggeredPeriod3 * 86400; + var period4 = folder.staggeredPeriod4 * 86400; + + switch (args) { + case '2': + if (period2 <= period1) { + return true; + } else { + return false; + } + break; + case '3': + if (period3 <= period2 || period2 <= period1) { + return true; + } else { + return false; + } + break; + case '4': + if (period4 <= period3 || period3 <= period2 || period2 <= period1) { + return true; + } else { + return false; + } + break; + case '5': + if (period5 <= period4 || period4 <= period3 || period3 <= period2 || period2 <= period1) { + return true; + } else { + return false; + } + break; + } + } + + $scope.getEnabledStaggeredIntervals = function (folder) { + var intervals = $translate.instant('Staggered Intervals') + ': ' + folder.versioning.params.staggeredInterval1 + 's/' + folder.versioning.params.staggeredPeriod1 / 60 + 'm'; + if (!$scope.isStaggeredIntervalDisabled('2', folder)) { + intervals += ', ' + folder.versioning.params.staggeredInterval2 / 3600 + 'h/' + folder.versioning.params.staggeredPeriod2 / 3600 + 'h' + } + if (!$scope.isStaggeredIntervalDisabled('3', folder)) { + intervals += ', ' + folder.versioning.params.staggeredInterval3 / 86400 + 'd/' + folder.versioning.params.staggeredPeriod3 / 86400 + 'd' + } + if (!$scope.isStaggeredIntervalDisabled('4', folder)) { + intervals += ', ' + folder.versioning.params.staggeredInterval4 / 86400 + 'd/' + folder.versioning.params.staggeredPeriod4 / 86400 + 'd' + } + if (!$scope.isStaggeredIntervalDisabled('5', folder)) { + intervals += ', ' + folder.versioning.params.staggeredInterval5 / 86400 + 'd/' + folder.versioning.params.maxAge / 86400 + 'd' + } + return intervals; + } + + $scope.countEnabledStaggeredIntervals = function (folder) { + var count = 1; + if (!$scope.isStaggeredIntervalDisabled('2', folder)) { + count += 1; + } + if (!$scope.isStaggeredIntervalDisabled('3', folder)) { + count += 1; + } + if (!$scope.isStaggeredIntervalDisabled('4', folder)) { + count += 1; + } + if (!$scope.isStaggeredIntervalDisabled('5', folder)) { + count += 1; + } + return count; + } + $scope.editFolderExisting = function (folderCfg, initialTab) { $scope.currentFolder = angular.copy(folderCfg); $scope.currentFolder._editing = "existing"; @@ -2351,6 +2474,33 @@ angular.module('syncthing.core') folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean; break; case "staggered": + // Fix invalid values with interval larger than period. This is + // a cosmetic change though as they are harmless due to only the + // oldest version in each interval being kept. + if (folderCfg._guiVersioning.staggeredInterval1 > folderCfg._guiVersioning.staggeredPeriod1 * 60) { + folderCfg._guiVersioning.staggeredInterval1 = folderCfg._guiVersioning.staggeredPeriod1 * 60; + } + if (folderCfg._guiVersioning.staggeredInterval2 > folderCfg._guiVersioning.staggeredPeriod2) { + folderCfg._guiVersioning.staggeredInterval2 = folderCfg._guiVersioning.staggeredPeriod2; + } + if (folderCfg._guiVersioning.staggeredInterval3 > folderCfg._guiVersioning.staggeredPeriod3) { + folderCfg._guiVersioning.staggeredInterval3 = folderCfg._guiVersioning.staggeredPeriod3; + } + if (folderCfg._guiVersioning.staggeredInterval4 > folderCfg._guiVersioning.staggeredPeriod4) { + folderCfg._guiVersioning.staggeredInterval4 = folderCfg._guiVersioning.staggeredPeriod4; + } + if (folderCfg._guiVersioning.staggeredInterval5 > folderCfg._guiVersioning.maxAge) { + folderCfg._guiVersioning.staggeredInterval5 = folderCfg._guiVersioning.maxAge; + } + folderCfg.versioning.params.staggeredInterval1 = '' + (folderCfg._guiVersioning.staggeredInterval1); + folderCfg.versioning.params.staggeredInterval2 = '' + (folderCfg._guiVersioning.staggeredInterval2 * 3600); + folderCfg.versioning.params.staggeredInterval3 = '' + (folderCfg._guiVersioning.staggeredInterval3 * 86400); + folderCfg.versioning.params.staggeredInterval4 = '' + (folderCfg._guiVersioning.staggeredInterval4 * 86400); + folderCfg.versioning.params.staggeredInterval5 = '' + (folderCfg._guiVersioning.staggeredInterval5 * 86400); + folderCfg.versioning.params.staggeredPeriod1 = '' + (folderCfg._guiVersioning.staggeredPeriod1 * 60); + folderCfg.versioning.params.staggeredPeriod2 = '' + (folderCfg._guiVersioning.staggeredPeriod2 * 3600); + folderCfg.versioning.params.staggeredPeriod3 = '' + (folderCfg._guiVersioning.staggeredPeriod3 * 86400); + folderCfg.versioning.params.staggeredPeriod4 = '' + (folderCfg._guiVersioning.staggeredPeriod4 * 86400); folderCfg.versioning.params.maxAge = '' + (folderCfg._guiVersioning.staggeredMaxAge * 86400); break; case "external": diff --git a/gui/default/syncthing/folder/editFolderModalView.html b/gui/default/syncthing/folder/editFolderModalView.html index 2faa72d570c..bb639f6ce0a 100644 --- a/gui/default/syncthing/folder/editFolderModalView.html +++ b/gui/default/syncthing/folder/editFolderModalView.html @@ -115,19 +115,95 @@ You must keep at least one version.

-
-

Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing. Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.

-

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.

- -
- -
days
+
+
+

+ Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing. + Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval. +

+

The following intervals are used by default:

+
    +
  • 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
  • +
  • for the first year a version is kept every week
  • +
  • until the maximum age a version is kept every month
  • +
+
+
+ +
+ +
days
+
+

+ The maximum time to keep a version (in days, set to 0 to keep versions forever). + The maximum age must be a number and cannot be blank. + A negative number of days doesn't make sense. +

+
+
+ +
+

+   + The intervals editor is intended for advanced users only! +

+

+ You can customize the intervals below. + 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. + The last time period is tied to both the maximum age and its preceeding interval and automatically changes with them. + Subsequent intervals are only enabled if their time period is larger than the previous ones. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Intervalminutes
Intervalhours
Intervaldays
Intervaldays
Interval + days
+
+

The interval must be a positive number of seconds.

+
-

- The maximum time to keep a version (in days, set to 0 to keep versions forever). - The maximum age must be a number and cannot be blank. - A negative number of days doesn't make sense. -

diff --git a/lib/versioner/staggered.go b/lib/versioner/staggered.go index 6f844b19555..702de970655 100644 --- a/lib/versioner/staggered.go +++ b/lib/versioner/staggered.go @@ -30,15 +30,51 @@ type interval struct { type staggered struct { folderFs fs.Filesystem versionsFs fs.Filesystem - interval [4]interval + interval [5]interval copyRangeMethod fs.CopyRangeMethod } func newStaggered(cfg config.FolderConfiguration) Versioner { params := cfg.Versioning.Params + interval1, err := strconv.ParseInt(params["staggeredInterval1"], 10, 0) + if err != nil { + interval1 = 30 // Default: 30 seconds + } + period1, err := strconv.ParseInt(params["staggeredPeriod1"], 10, 0) + if err != nil { + period1 = 3600 // Default: 1 minute + } + interval2, err := strconv.ParseInt(params["staggeredInterval2"], 10, 0) + if err != nil { + interval2 = 3600 // Default: 1 hour + } + period2, err := strconv.ParseInt(params["staggeredPeriod2"], 10, 0) + if err != nil { + period2 = 86400 // Default: 1 day + } + interval3, err := strconv.ParseInt(params["staggeredInterval3"], 10, 0) + if err != nil { + interval3 = 86400 // Default: 1 day + } + period3, err := strconv.ParseInt(params["staggeredPeriod3"], 10, 0) + if err != nil { + period3 = 2592000 // Default: 1 month + } + interval4, err := strconv.ParseInt(params["staggeredInterval4"], 10, 0) + if err != nil { + interval4 = 604800 // Default: 1 week + } + period4, err := strconv.ParseInt(params["staggeredPeriod4"], 10, 0) + if err != nil { + period4 = 31536000 // Default: 1 year + } + interval5, err := strconv.ParseInt(params["staggeredInterval5"], 10, 0) + if err != nil { + interval5 = 2592000 // Default: 1 month + } maxAge, err := strconv.ParseInt(params["maxAge"], 10, 0) if err != nil { - maxAge = 31536000 // Default: ~1 year + maxAge = 31536000 // Default: 1 year } versionsFs := versionerFsFromFolderCfg(cfg) @@ -46,11 +82,12 @@ func newStaggered(cfg config.FolderConfiguration) Versioner { s := &staggered{ folderFs: cfg.Filesystem(nil), versionsFs: versionsFs, - interval: [4]interval{ - {30, 60 * 60}, // first hour -> 30 sec between versions - {60 * 60, 24 * 60 * 60}, // next day -> 1 h between versions - {24 * 60 * 60, 30 * 24 * 60 * 60}, // next 30 days -> 1 day between versions - {7 * 24 * 60 * 60, maxAge}, // next year -> 1 week between versions + interval: [5]interval{ + {interval1, period1}, // first hour -> 30 sec between versions + {interval2, period2}, // first day -> 1 h between versions + {interval3, period3}, // first 30 days -> 1 day between versions + {interval4, period4}, // first year -> 1 week between versions + {interval5, maxAge}, // next year -> 1 month between versions }, copyRangeMethod: cfg.CopyRangeMethod, } diff --git a/lib/versioner/staggered_test.go b/lib/versioner/staggered_test.go index 21122ab5e48..8695d797e80 100644 --- a/lib/versioner/staggered_test.go +++ b/lib/versioner/staggered_test.go @@ -22,10 +22,11 @@ import ( func TestStaggeredVersioningVersionCount(t *testing.T) { /* Default settings: - {30, 3600}, // first hour -> 30 sec between versions - {3600, 86400}, // next day -> 1 h between versions - {86400, 592000}, // next 30 days -> 1 day between versions - {604800, maxAge}, // next year -> 1 week between versions + {30, 3600}, // first hour -> 30 sec between versions + {3600, 86400}, // first day -> 1 h between versions + {86400, 2592000}, // first 30 days -> 1 day between versions + {604800, 31536000}, // first year -> 1 week between versions + {2592000, maxAge}, // next year -> 1 month between versions */ now := parseTime("20160415-140000") @@ -76,6 +77,7 @@ func TestStaggeredVersioningVersionCount(t *testing.T) { "test~20150416-135959", // 365 days 1 second ago "test~20150416-135958", // 365 days 2 seconds ago "test~20150414-140000", // 367 days ago + "test~20140415-140000", // 2 years ago } delete := []string{ @@ -96,6 +98,7 @@ func TestStaggeredVersioningVersionCount(t *testing.T) { "test~20150416-135959", // 365 days 1 second ago "test~20150416-135958", // 365 days 2 seconds ago "test~20150414-140000", // 367 days ago + "test~20140415-140000", // 2 years ago } sort.Strings(delete)