Merge branch 'master' into experiment/quickfix_concurrent

This commit is contained in:
Kenneth Skovhede
2018-02-01 09:53:48 +01:00
24 changed files with 298 additions and 101 deletions
+58 -16
View File
@@ -254,33 +254,75 @@ namespace Duplicati.Library.Backend
{
int offset = 0;
int count = FILE_LIST_PAGE_SIZE;
int numFiles = 0;
int filesOk = 0;
int filesRepeated = 0;
int iteration = 0;
var files = new List<IFileEntry>();
m_fileidCache.Clear();
while(count == FILE_LIST_PAGE_SIZE)
do
{
var url = string.Format("{0}/{1}?access_token={2}&limit={3}&offset={4}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, FolderID), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken), FILE_LIST_PAGE_SIZE, offset);
var res = m_oauth.GetJSONData<WLID_DataItem>(url, x => x.UserAgent = USER_AGENT);
if (res != null && res.data != null)
while (count == FILE_LIST_PAGE_SIZE)
{
count = res.data.Length;
foreach(var r in res.data)
var url = string.Format("{0}/{1}?access_token={2}&limit={3}&offset={4}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, FolderID), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken), FILE_LIST_PAGE_SIZE, offset);
var res = m_oauth.GetJSONData<WLID_DataItem>(url);
if (res != null && res.data != null)
{
m_fileidCache.Add(r.name, r.id);
count = res.data.Length;
var fe = new FileEntry(r.name, r.size.Value, r.updated_time.Value, r.updated_time.Value);
fe.IsFolder = string.Equals(r.type, "folder", StringComparison.OrdinalIgnoreCase);
yield return fe;
// log
Console.WriteLine("Iteration: {0:D} Offset: {1:D} Count: {2:D} TotalOK: {3:D} TotalRep: {4:D} TotalFiles: {5:D}", iteration, offset, count, filesOk, filesRepeated, numFiles);
foreach (var r in res.data)
{
if (m_fileidCache.ContainsKey(r.name))
{
filesRepeated++;
}
else
{
m_fileidCache.Add(r.name, r.id);
var fe = new FileEntry(r.name, r.size.Value, r.updated_time.Value, r.updated_time.Value);
fe.IsFolder = string.Equals(r.type, "folder", StringComparison.OrdinalIgnoreCase);
files.Add(fe);
filesOk++;
}
}
}
}
else
{
count = 0;
else
{
count = 0;
}
if (iteration != 0 && filesOk == numFiles) return files;
offset += count;
}
offset += count;
// Save total number of files in the first iteration
if (iteration == 0)
{
numFiles = offset;
}
filesRepeated = 0;
iteration++;
offset = 0;
count = FILE_LIST_PAGE_SIZE;
}
while (filesOk != numFiles);
return files;
}
public void Put(string remotename, string filename)
@@ -144,40 +144,46 @@ namespace Duplicati.Library.Main.Operation
/// Gets the filesets selected for deletion
/// </summary>
/// <returns>The filesets to delete</returns>
/// <param name="backups">The list of backups that can be deleted</param>
private DateTime[] GetFilesetsToDelete(DateTime[] backups)
/// <param name="allBackups">The list of backups that can be deleted</param>
private DateTime[] GetFilesetsToDelete(DateTime[] allBackups)
{
if (backups.Length == 0)
return backups;
if (allBackups.Length == 0)
return allBackups;
if (backups.Distinct().Count() != backups.Length)
throw new Exception(string.Format("List of backup timestamps contains duplicates: {0}", string.Join(", ", backups.Select(x => x.ToString()))));
if (allBackups.Distinct().Count() != allBackups.Length)
throw new Exception(string.Format("List of backup timestamps contains duplicates: {0}", string.Join(", ", allBackups.Select(x => x.ToString()))));
List<DateTime> res = new List<DateTime>();
List<DateTime> toDelete = new List<DateTime>();
// Remove backups explicitely specified via option
var versions = m_options.Version;
if (versions != null && versions.Length > 0)
foreach (var ix in versions.Distinct())
if (ix >= 0 && ix < backups.Length)
res.Add(backups[ix]);
var keepVersions = m_options.KeepVersions;
if (keepVersions > 0 && keepVersions < backups.Length)
res.AddRange(backups.Skip(keepVersions));
if (ix >= 0 && ix < allBackups.Length)
toDelete.Add(allBackups[ix]);
// Remove backups that are older than date specified via option
var keepTime = m_options.KeepTime;
if (keepTime.Ticks > 0)
res.AddRange(backups.SkipWhile(x => x >= keepTime));
toDelete.AddRange(allBackups.SkipWhile(x => x >= keepTime));
res.AddRange(ApplyRetentionPolicy(backups));
// Remove backups via rentention policy option
toDelete.AddRange(ApplyRetentionPolicy(allBackups));
var filtered = res.Distinct().OrderByDescending(x => x).AsEnumerable();
// Check how many backups will be remaining after the previous steps
// and remove oldest backups while there are still more backups than should be kept as specified via option
var backupsRemaining = allBackups.Except(toDelete).ToList();
var keepVersions = m_options.KeepVersions;
if (keepVersions > 0 && keepVersions < backupsRemaining.Count())
toDelete.AddRange(backupsRemaining.Skip(keepVersions));
var removeCount = filtered.Count();
if (removeCount > backups.Length)
throw new Exception(string.Format("Too many entries {0} vs {1}, lists: {2} vs {3}", removeCount, backups.Length, string.Join(", ", filtered.Select(x => x.ToString())), string.Join(", ", backups.Select(x => x.ToString()))));
var toDeleteDistinct = toDelete.Distinct().OrderByDescending(x => x).AsEnumerable();
return filtered.ToArray();
var removeCount = toDeleteDistinct.Count();
if (removeCount > allBackups.Length)
throw new Exception(string.Format("Too many entries {0} vs {1}, lists: {2} vs {3}", removeCount, allBackups.Length, string.Join(", ", toDeleteDistinct.Select(x => x.ToString())), string.Join(", ", allBackups.Select(x => x.ToString()))));
return toDeleteDistinct.ToArray();
}
/// <summary>
@@ -48,7 +48,7 @@ namespace Duplicati.Library.Main.Volumes
: base(options)
{
if (!string.IsNullOrWhiteSpace(options.AsynchronousUploadFolder))
m_localfile = Library.Utility.TempFile.CreateInFolder(options.AsynchronousUploadFolder);
m_localfile = Library.Utility.TempFile.CreateInFolder(options.AsynchronousUploadFolder, true);
else
m_localfile = new Library.Utility.TempFile();
+4 -1
View File
@@ -161,8 +161,11 @@ namespace Duplicati.Library.Utility
return new TempFile(path);
}
public static TempFile CreateInFolder(string path)
public static TempFile CreateInFolder(string path, bool autocreatefolder)
{
if (autocreatefolder && !System.IO.Directory.Exists(path))
System.IO.Directory.CreateDirectory(path);
return new TempFile(System.IO.Path.Combine(path, GenerateUniqueName()));
}
+121 -5
View File
@@ -153,7 +153,7 @@ namespace Duplicati.Server
return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never);
}
public static int RealMain(string[] args)
public static int RealMain(string[] _args)
{
//If we are on Windows, append the bundled "win-tools" programs to the search path
//We add it last, to allow the user to override with other versions
@@ -170,11 +170,14 @@ namespace Duplicati.Server
//If this executable is invoked directly, write to console, otherwise throw exceptions
var writeConsole = System.Reflection.Assembly.GetEntryAssembly() == System.Reflection.Assembly.GetExecutingAssembly();
//Find commandline options here for handling special startup cases
var commandlineOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(new List<string>(args));
foreach(var s in args)
//Find commandline options here for handling special startup cases
var args = new List<string>(_args);
var argtuple = Library.Utility.FilterCollector.ExtractOptions(new List<string>(args));
var commandlineOptions = argtuple.Item1;
var filter = argtuple.Item2;
foreach(var s in _args)
if (
s.Equals("help", StringComparison.OrdinalIgnoreCase) ||
s.Equals("/help", StringComparison.OrdinalIgnoreCase) ||
@@ -182,6 +185,33 @@ namespace Duplicati.Server
s.Equals("/usage", StringComparison.OrdinalIgnoreCase))
commandlineOptions["help"] = "";
// Check if a parameters-file was provided. Skip if help was already specified
if (!commandlineOptions.ContainsKey("help"))
{
string filename = null;
if (commandlineOptions.ContainsKey("parameters-file"))
{
filename = commandlineOptions["parameters-file"];
commandlineOptions.Remove("parameters-file");
}
else if (commandlineOptions.ContainsKey("parameter-file"))
{
filename = commandlineOptions["parameter-file"];
commandlineOptions.Remove("parameter-file");
}
else if (commandlineOptions.ContainsKey("parameterfile"))
{
filename = commandlineOptions["parameterfile"];
commandlineOptions.Remove("parameterfile");
}
if (!string.IsNullOrEmpty(filename))
{
// Check the parameters-file and add provided arguments (overriding commandline arguments)
ReadOptionsFromFile(filename, ref filter, args, commandlineOptions);
}
}
//If the commandline issues --help, just stop here
if (commandlineOptions.ContainsKey("help"))
{
@@ -759,6 +789,7 @@ namespace Duplicati.Server
{
var lst = new List<Duplicati.Library.Interface.ICommandLineArgument> (new Duplicati.Library.Interface.ICommandLineArgument[] {
new Duplicati.Library.Interface.CommandLineArgument("help", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription),
new Duplicati.Library.Interface.CommandLineArgument("parameters-file", Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong2, "", new string[] {"parameter-file", "parameterfile"}),
new Duplicati.Library.Interface.CommandLineArgument("unencrypted-database", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.UnencrypteddatabaseCommandDescription, Strings.Program.UnencrypteddatabaseCommandDescription),
new Duplicati.Library.Interface.CommandLineArgument("portable-mode", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription),
new Duplicati.Library.Interface.CommandLineArgument("log-file", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription),
@@ -780,5 +811,90 @@ namespace Duplicati.Server
return lst.ToArray();
}
}
private static bool ReadOptionsFromFile(string filename, ref Library.Utility.IFilter filter, List<string> cargs, Dictionary<string, string> options)
{
try
{
List<string> fargs = new List<string>(Library.Utility.Utility.ReadFileWithDefaultEncoding(Library.Utility.Utility.ExpandEnvironmentVariables(filename)).Replace("\r\n", "\n").Replace("\r", "\n").Split(new String[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()));
var newsource = new List<string>();
string newtarget = null;
string prependfilter = null;
string appendfilter = null;
string replacefilter = null;
var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(fargs, (key, value) => {
if (key.Equals("source", StringComparison.OrdinalIgnoreCase))
{
newsource.Add(value);
return false;
}
else if (key.Equals("target", StringComparison.OrdinalIgnoreCase))
{
newtarget = value;
return false;
}
else if (key.Equals("append-filter", StringComparison.OrdinalIgnoreCase))
{
appendfilter = value;
return false;
}
else if (key.Equals("prepend-filter", StringComparison.OrdinalIgnoreCase))
{
prependfilter = value;
return false;
}
else if (key.Equals("replace-filter", StringComparison.OrdinalIgnoreCase))
{
replacefilter = value;
return false;
}
return true;
});
var opt = tmpparsed.Item1;
var newfilter = tmpparsed.Item2;
// If the user specifies parameters-file, all filters must be in the file.
// Allowing to specify some filters on the command line could result in wrong filter ordering
if (!filter.Empty && !newfilter.Empty)
throw new Duplicati.Library.Interface.UserInformationException(Strings.Program.FiltersCannotBeUsedWithFileError2);
if (!newfilter.Empty)
filter = newfilter;
if (!string.IsNullOrWhiteSpace(prependfilter))
filter = Library.Utility.FilterExpression.Combine(Library.Utility.FilterExpression.Deserialize(prependfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)), filter);
if (!string.IsNullOrWhiteSpace(appendfilter))
filter = Library.Utility.FilterExpression.Combine(filter, Library.Utility.FilterExpression.Deserialize(appendfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)));
if (!string.IsNullOrWhiteSpace(replacefilter))
filter = Library.Utility.FilterExpression.Deserialize(replacefilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries));
foreach (KeyValuePair<String, String> keyvalue in opt)
options[keyvalue.Key] = keyvalue.Value;
if (!string.IsNullOrEmpty(newtarget))
{
if (cargs.Count <= 1)
cargs.Add(newtarget);
else
cargs[1] = newtarget;
}
if (cargs.Count >= 1 && cargs[0].Equals("backup", StringComparison.OrdinalIgnoreCase))
cargs.AddRange(newsource);
else if (newsource.Count > 0 && Library.Utility.Utility.ParseBoolOption(options, "verbose"))
Console.WriteLine(Strings.Program.SkippingSourceArgumentsOnNonBackupOperation);
return true;
}
catch (Exception e)
{
throw new Exception(Strings.Program.FailedToParseParametersFileError(filename, e.Message));
}
}
}
}
+6
View File
@@ -245,6 +245,12 @@ namespace Duplicati.Server
try
{
// Recover from timedrift issues by overriding the dates if the last run date is in the future.
if (last > DateTime.UtcNow)
{
start = DateTime.UtcNow;
last = DateTime.UtcNow;
}
start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays);
}
catch (Exception ex)
+5
View File
@@ -12,6 +12,11 @@ Error message: {0}", message); }
"); } }
public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); }
public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
public static string LogfileCommandDescription { get { return LC.L(@"Outputs log information to the file given"); } }
public static string LoglevelCommandDescription { get { return LC.L(@"Determines the amount of information written in the log file"); } }
public static string PortablemodeCommandDescription { get { return LC.L(@"Activates portable mode where the database is placed below the program executable"); } }
@@ -55,6 +55,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
var input = info.Request.Form;
var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false);
var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false);
var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false);
output_template = output_template.Replace("CBM", input["callback"].Value);
if (cmdline)
@@ -90,6 +91,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
}
}
if (!import_metadata) {
ipx.Backup.Metadata.Clear();
}
ipx.Backup.ID = null;
((Database.Backup)ipx.Backup).DBPath = null;
+10 -2
View File
@@ -212,7 +212,6 @@ ul.tabs {
.entries.livedata {
li {
height: 1.2em;
overflow: hidden;
}
li.expanded {
@@ -1492,7 +1491,7 @@ body {
ul.entries {
li {
padding-top: 15px;
padding-top: 30px;
}
}
}
@@ -1820,6 +1819,15 @@ div.modal-dialog {
}
}
.importpage {
form.styled {
input {
margin-top: 11px;
margin-bottom: 11px;
}
}
}
.restorewizard,
.addwizard {
form.styled {
File diff suppressed because one or more lines are too long
@@ -119,6 +119,14 @@ backupApp.directive('advancedOptionsEditor', function() {
return item.LongDescription;
};
$scope.getDefaultValue = function(item) {
var item = $scope.getEntry(item);
if (item == null)
return null;
return item.DefaultValue;
};
$scope.getEnumerations = function(item) {
var item = $scope.getEntry(item);
if (item == null)
@@ -201,26 +201,17 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
if (scope.openstack_providers == null) {
AppService.post('/webmodule/openstack-getconfig', {'openstack-config': 'Providers'}).then(function(data) {
scope.openstack_providers = data.data.Result;
if (scope.openstack_providers != null && scope.openstack_server == undefined && scope.openstack_server_custom == undefined)
var first = null;
var rs = null;
for (var k in scope.openstack_providers) {
if (first == null)
first = scope.openstack_providers[k];
if (scope.openstack_providers[k] != null && scope.openstack_providers[k].indexOf('https://identity.api.rackspacecloud.com/') == 0)
rs = scope.openstack_providers[k];
}
if (rs != null)
scope.openstack_server = rs;
else if (first != null)
scope.openstack_server = first;
if (scope.openstack_server == undefined && scope.openstack_server_custom == undefined)
scope.openstack_server = 'https://identity.api.rackspacecloud.com/';
}, AppUtils.connectionError);
} else {
if (scope.openstack_server == undefined && scope.openstack_server_custom == undefined)
scope.openstack_server = 'https://identity.api.rackspacecloud.com/';
}
};
EditUriBackendConfig.loaders['gcs'] = function(scope) {
if (scope.gcs_locations == null) {
AppService.post('/webmodule/gcs-getconfig', {'gcs-config': 'Locations'}).then(function(data) {
@@ -1,37 +1,37 @@
<ul class="advancedoptions">
<li ng-repeat="item in ngModel" class="advancedentry">
<li ng-repeat="item in ngModel | orderBy: item as nn track by $index" class="advancedentry">
<div>
<label class="shortname">{{getShortName(item)}}</label>
<select ng-if="getInputType(item) == 'enum'" ng-options="n for n in getEnumerations(item)" ng-model="$parent.ngModel[$index]" parse-advanced-option="getEnumerations(item)"></select>
<select ng-if="getInputType(item) == 'flags'" ng-options="n for n in getEnumerations(item)" ng-model="$parent.ngModel[$index]" parse-advanced-option-flags="getEnumerations(item)" multiple="multiple"></select>
<input ng-if="getInputType(item) == 'bool'" type="checkbox" ng-model="$parent.ngModel[$index]" parse-advanced-option-bool ></input>
<input ng-if="getInputType(item) == 'password'" type="password" ng-model="$parent.ngModel[$index]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'text'" type="text" ng-model="$parent.ngModel[$index]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'int'" type="number" ng-model="$parent.ngModel[$index]" parse-advanced-option-integer ></input>
<select ng-if="getInputType(item) == 'enum'" ng-options="n for n in getEnumerations(item)" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option="getEnumerations(item)"></select>
<select ng-if="getInputType(item) == 'flags'" ng-options="n for n in getEnumerations(item)" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option-flags="getEnumerations(item)" multiple="multiple"></select>
<input ng-if="getInputType(item) == 'bool'" type="checkbox" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option-bool ></input>
<input ng-if="getInputType(item) == 'password'" type="password" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'text'" type="text" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'int'" type="number" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" parse-advanced-option-integer ></input>
<div ng-if="getInputType(item) == 'size'" class="input type multiple">
<input type="number" parse-advanced-option-size-number="uppercase" ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier="uppercase" ng-model="$parent.ngModel[$index]" ng-options="item.value as item.name for item in fileSizeMultipliers">
<input type="number" parse-advanced-option-size-number="uppercase" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" />
<select parse-advanced-option-size-multiplier="uppercase" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" ng-options="item.value as item.name for item in fileSizeMultipliers">
</select>
</div>
<div ng-if="getInputType(item) == 'speed'" class="input type multiple">
<input type="number" parse-advanced-option-size-number="uppercase" ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier="uppercase" ng-model="$parent.ngModel[$index]" ng-options="item.value as item.name for item in speedMultipliers">
<input type="number" parse-advanced-option-size-number="uppercase" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" />
<select parse-advanced-option-size-multiplier="uppercase" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" ng-options="item.value as item.name for item in speedMultipliers">
</select>
</div>
<div ng-if="getInputType(item) == 'timespan'" class="input type multiple">
<input parse-advanced-option-size-number type="number" ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$index]" ng-options="item.value as item.name for item in timerangeMultipliers">
<input parse-advanced-option-size-number type="number" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" ng-options="item.value as item.name for item in timerangeMultipliers">
<option value="" translate>custom</option>
</select>
</div>
<div ng-if="getInputType(item) == 'shorttimespan'" class="input type multiple">
<input parse-advanced-option-size-number type="number" ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$index]" ng-options="item.value as item.name for item in shorttimerangeMultipliers">
<input parse-advanced-option-size-number type="number" ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$parent.ngModel.indexOf(item)]" ng-options="item.value as item.name for item in shorttimerangeMultipliers">
<option value="" translate>custom</option>
</select>
</div>
@@ -39,7 +39,7 @@
<a href ng-click="deleteItem(item)" title="{{'Remove option' | translate}}">x</a>
<div class="longdescription">{{getLongDescription(item)}}</div>
<div class="longdescription">{{getLongDescription(item)}}<br>Default value: "{{getDefaultValue(item)}}"</div>
</li>
<li>
@@ -1,5 +1,5 @@
<div ng-controller="ImportController" class="importpage">
<h1 ng-hide="restoremode" translate>Import backup configuration</h1>
<h1 ng-show="restoremode" translate>Restore from backup configuration</h1>
@@ -15,6 +15,11 @@
<input type="password" name="passphrase" id="passphrase" placeholder="{{'Enter encryption passphrase' | translate}}" />
</div>
<div class="input checkbox" ng-hide="restoremode">
<label for="import_metadata" translate>Import metadata</label>
<input type="checkbox" name="import_metadata" id="import_metadata" value="true" />
</div>
<div class="input checkbox" ng-hide="restoremode">
<label for="direct" translate>Save immediately</label>
<input type="checkbox" name="direct" id="direct" value="true" />
@@ -25,7 +30,7 @@
<div class="buttons" ng-hide="Connecting">
<a href ng-click="doSubmit()" translate>Import</a>
</div>
<div class="buttons" ng-show="Connecting">
<a href translate>Importing ...</a>
</div>
-1
View File
@@ -22,7 +22,6 @@ using System.Collections.Generic;
namespace Duplicati.UnitTest
{
[TestFixture]
public class BorderTests : BasicSetupHelper
{
public override void PrepareSourceData()
@@ -23,7 +23,6 @@ using System.Reflection;
namespace Duplicati.UnitTest
{
[TestFixture]
public class CommandLineOperationsTests : BasicSetupHelper
{
/// <summary>
-1
View File
@@ -22,7 +22,6 @@ using NUnit.Framework;
namespace Duplicati.UnitTest
{
[TestFixture]
[Category("Compression")]
public class CompressionTests
{
@@ -23,7 +23,6 @@ using System.Reflection;
namespace Duplicati.UnitTest
{
[TestFixture]
public class GeneralBlackBoxTesting
{
private static readonly string SOURCE_FOLDERS =
-1
View File
@@ -22,7 +22,6 @@ using System.Collections.Generic;
namespace Duplicati.UnitTest
{
[TestFixture]
public class Issue1410 : BasicSetupHelper
{
[OneTimeSetUp]
-1
View File
@@ -22,7 +22,6 @@ using NUnit.Framework;
namespace Duplicati.UnitTest
{
[TestFixture]
public class PurgeTesting : BasicSetupHelper
{
public override void PrepareSourceData()
-1
View File
@@ -21,7 +21,6 @@ using System.Collections.Generic;
namespace Duplicati.UnitTest
{
[TestFixture]
public class UtilityTests
{
[Test]
+2 -2
View File
@@ -100,7 +100,7 @@ git checkout INFO
echo "version=\"${VERSION}\"" >> "INFO"
MD5=`md5 "package.tgz" | awk -F ' ' '{print $NF}'`
echo "checksum=\"${MD5}\"" >> "INFO"
echo "extractsize=\"${DIRSIZE}\"" >> "INFO"
echo "extractsize=\"${DIRSIZE_KB}\"" >> "INFO"
echo "package_icon=\"${ICON_72}\"" >> "INFO"
echo "package_icon_256=\"${ICON_256}\"" >> "INFO"
@@ -145,4 +145,4 @@ if [ "z${GPGID}" != "z" ]; then
tar cf "${BASE_FILE_NAME}.spk" -C "${TMPDIRNAME}" `ls -1 ${TMPDIRNAME}`
rm -rf "${TMPDIRNAME}"
fi
fi
+1 -1
View File
@@ -1 +1 @@
16
17
+10
View File
@@ -1,3 +1,13 @@
2018-01-23 - 2.0.2.17_canary_2018-01-23
==========
- Fixed a bug with the update packages having an invalid hash
- Updated external links in the UI to be clearly marked
- Experimental support for Google Team Drives
- Fixed WEBDAV Test function, thanks @inat3k
- Added an rclone backend, thanks @wjansenw
- Fixed some broken error messages, thanks @JonMikeIV
- Added a simple UI for exposing the `--retention-policy` option
2018-01-18 - 2.0.2.16_canary_2018-01-18
==========
* Reworked some internal compression code, thanks @vnau