Merge branch 'master' into experiment/quickfix_concurrent

# Conflicts:
#	Duplicati/Library/Main/BackendManager.cs
#	Duplicati/Library/Main/Database/ExtensionMethods.cs
#	Duplicati/Library/Main/Operation/BackupHandler.cs
#	Duplicati/Library/Main/Operation/TestFilterHandler.cs
#	Duplicati/Library/Main/Options.cs

Manually ported the `IsSynlink` changes as well as the DNS refresh code
This commit is contained in:
Kenneth Skovhede
2018-03-02 09:06:13 +01:00
166 changed files with 11567 additions and 8043 deletions
+24 -25
View File
@@ -588,37 +588,36 @@ namespace Duplicati.CommandLine
if (output.VerboseOutput)
{
Library.Utility.Utility.PrintSerializeObject(result, outwriter);
outwriter.WriteLine();
}
else
var parsedStats = result.BackendStatistics as Duplicati.Library.Interface.IParsedBackendStatistics;
output.MessageEvent(string.Format(" Duration of backup: {0:hh\\:mm\\:ss}", result.Duration));
if (parsedStats != null)
{
var parsedStats = result.BackendStatistics as Duplicati.Library.Interface.IParsedBackendStatistics;
output.MessageEvent(string.Format(" Duration of backup: {0:hh\\:mm\\:ss}", result.Duration));
if (parsedStats != null)
if (parsedStats.KnownFileCount > 0)
{
if (parsedStats.KnownFileCount > 0)
{
output.MessageEvent(string.Format(" Remote files: {0}", parsedStats.KnownFileCount));
output.MessageEvent(string.Format(" Remote size: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.KnownFileSize)));
}
if (parsedStats.TotalQuotaSpace >= 0)
{
output.MessageEvent(string.Format(" Total remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.TotalQuotaSpace)));
}
if (parsedStats.FreeQuotaSpace >= 0)
{
output.MessageEvent(string.Format(" Available remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.FreeQuotaSpace)));
}
output.MessageEvent(string.Format(" Remote files: {0}", parsedStats.KnownFileCount));
output.MessageEvent(string.Format(" Remote size: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.KnownFileSize)));
}
output.MessageEvent(string.Format(" Files added: {0}", result.AddedFiles));
output.MessageEvent(string.Format(" Files deleted: {0}", result.DeletedFiles));
output.MessageEvent(string.Format(" Files changed: {0}", result.ModifiedFiles));
output.MessageEvent(string.Format(" Data uploaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesUploaded)));
output.MessageEvent(string.Format(" Data downloaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesDownloaded)));
if (parsedStats.TotalQuotaSpace >= 0)
{
output.MessageEvent(string.Format(" Total remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.TotalQuotaSpace)));
}
if (parsedStats.FreeQuotaSpace >= 0)
{
output.MessageEvent(string.Format(" Available remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.FreeQuotaSpace)));
}
}
output.MessageEvent(string.Format(" Files added: {0}", result.AddedFiles));
output.MessageEvent(string.Format(" Files deleted: {0}", result.DeletedFiles));
output.MessageEvent(string.Format(" Files changed: {0}", result.ModifiedFiles));
output.MessageEvent(string.Format(" Data uploaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesUploaded)));
output.MessageEvent(string.Format(" Data downloaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesDownloaded)));
if (result.ExaminedFiles == 0 && (filter != null || !filter.Empty))
output.MessageEvent("No files were processed. If this was not intentional you may want to use the \"test-filters\" command");
@@ -73,10 +73,10 @@
<HintPath>..\..\..\thirdparty\appindicator-sharp\appindicator-sharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="gdk-sharp">
<Reference Include="gdk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f">
<HintPath>..\..\..\thirdparty\appindicator-sharp\gdk-sharp.dll</HintPath>
</Reference>
<Reference Include="gtk-sharp">
<Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f">
<HintPath>..\..\..\thirdparty\appindicator-sharp\gtk-sharp.dll</HintPath>
</Reference>
<Reference Include="glib-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
@@ -450,6 +450,11 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
}
public string[] DNSName
{
get { return new string[] { new Uri(_url).Host }; }
}
private static System.IO.Stream StringToStream(string str)
{
var stream = new System.IO.MemoryStream();
@@ -457,6 +457,30 @@ namespace Duplicati.Library.Backend.AmazonCloudDrive
return Strings.AmzCD.Description;
}
}
public string[] DNSName
{
get
{
var contentUrl = string.Empty;
var metdataUrl = string.Empty;
if (m_endPointInfo != null)
{
if (!string.IsNullOrWhiteSpace(m_endPointInfo.ContentUrl))
contentUrl = new Uri(m_endPointInfo.ContentUrl).Host;
if (!string.IsNullOrWhiteSpace(m_endPointInfo.MetadataUrl))
metdataUrl = new Uri(m_endPointInfo.MetadataUrl).Host;
}
return new string[] {
new Uri(CLOUDRIVE_MASTER_URL).Host,
contentUrl,
metdataUrl
};
}
}
#endregion
#region IDisposable implementation
public void Dispose()
@@ -157,8 +157,13 @@ namespace Duplicati.Library.Backend.AzureBlob
{
return Strings.AzureBlobBackend.Description_v2;
}
}
}
public string[] DNSName
{
get { return _azureBlob.DnsNames; }
}
public void Test()
{
this.TestList();
@@ -36,6 +36,29 @@ namespace Duplicati.Library.Backend.AzureBlob
private readonly string _containerName;
private readonly CloudBlobContainer _container;
public string[] DnsNames
{
get
{
var lst = new List<string>();
if (_container != null)
{
if (_container.Uri != null)
lst.Add(_container.Uri.Host);
if (_container.StorageUri != null)
{
if (_container.StorageUri.PrimaryUri != null)
lst.Add(_container.StorageUri.PrimaryUri.Host);
if (_container.StorageUri.SecondaryUri != null)
lst.Add(_container.StorageUri.SecondaryUri.Host);
}
}
return lst.ToArray();
}
}
public AzureBlobWrapper(string accountName, string accessKey, string containerName)
{
_containerName = containerName;
@@ -422,6 +422,11 @@ namespace Duplicati.Library.Backend.Backblaze
get { return Strings.B2.Description; }
}
public string[] DNSName
{
get { return new string[] { new System.Uri(B2AuthHelper.AUTH_URL).Host, m_helper?.APIDnsName, m_helper?.DownloadDnsName} ; }
}
public void Dispose()
{
}
@@ -27,7 +27,7 @@ namespace Duplicati.Library.Backend.Backblaze
private readonly string m_credentials;
private AuthResponse m_config;
private DateTime m_configExpires;
private const string AUTH_URL = "https://api.backblazeb2.com/b2api/v1/b2_authorize_account";
internal const string AUTH_URL = "https://api.backblazeb2.com/b2api/v1/b2_authorize_account";
public B2AuthHelper(string userid, string password)
: base()
@@ -54,6 +54,26 @@ namespace Duplicati.Library.Backend.Backblaze
return url;
}
public string APIDnsName
{
get
{
if (m_config == null || string.IsNullOrWhiteSpace(m_config.APIUrl))
return null;
return new System.Uri(m_config.APIUrl).Host;
}
}
public string DownloadDnsName
{
get
{
if (m_config == null || string.IsNullOrWhiteSpace(m_config.DownloadUrl))
return null;
return new System.Uri(m_config.DownloadUrl).Host;
}
}
private AuthResponse Config
{
get
+7 -1
View File
@@ -145,7 +145,8 @@ namespace Duplicati.Library.Backend.Box
if (m_filecache != null && m_filecache.ContainsKey(name))
return m_filecache[name];
PagedFileListResponse(CurrentFolder, false);
// Make sure we enumerate this, otherwise the m_filecache is not assigned
PagedFileListResponse(CurrentFolder, false).LastOrDefault();
if (m_filecache != null && m_filecache.ContainsKey(name))
return m_filecache[name];
@@ -336,6 +337,11 @@ namespace Duplicati.Library.Backend.Box
}
}
public string[] DNSName
{
get { return new string[] { new Uri(BOX_API_URL).Host, new Uri(BOX_UPLOAD_URL).Host }; }
}
#endregion
#region IDisposable implementation
@@ -290,6 +290,11 @@ namespace Duplicati.Library.Backend
get { return true; }
}
public string[] DNSName
{
get { return new string[] { new Uri(m_authUrl).Host, string.IsNullOrWhiteSpace(m_storageUrl) ? null : new Uri(m_storageUrl).Host }; }
}
public void Get(string remotename, System.IO.Stream stream)
{
var req = CreateRequest("/" + remotename, "");
@@ -136,6 +136,11 @@ namespace Duplicati.Library.Backend
public string Description { get { return Strings.Dropbox.Description; } }
public string[] DNSName
{
get { return new string[] { new Uri(DropboxHelper.API_URL).Host, new Uri(DropboxHelper.CONTENT_API_URL).Host }; }
}
public void Test()
{
this.TestList();
@@ -11,8 +11,8 @@ namespace Duplicati.Library.Backend
{
public class DropboxHelper : OAuthHelper
{
private const string API_URL = "https://api.dropboxapi.com/2";
private const string CONTENT_API_URL = "https://content.dropboxapi.com/2";
internal const string API_URL = "https://api.dropboxapi.com/2";
internal const string CONTENT_API_URL = "https://content.dropboxapi.com/2";
private const int DROPBOX_MAX_CHUNK_UPLOAD = 10 * 1024 * 1024; // 10 MB max upload
private const string API_ARG_HEADER = "DROPBOX-API-arg";
@@ -355,6 +355,11 @@ namespace Duplicati.Library.Backend
}
}
public string[] DNSName
{
get { return new string[] { new Uri(m_url).Host }; }
}
public void Test()
{
this.TestList();
@@ -351,6 +351,11 @@ namespace Duplicati.Library.Backend
}
}
public string[] DNSName
{
get { return null; }
}
public void Rename(string oldname, string newname)
{
var source = GetRemoteName(oldname);
@@ -260,6 +260,12 @@ namespace Duplicati.Library.Backend.GoogleCloudStorage
{
get { return Strings.GoogleCloudStorage.Description; }
}
public string[] DNSName
{
get { return new string[] { new System.Uri(UPLOAD_API_URL).Host, new System.Uri(API_URL).Host }; }
}
#endregion
public void Put(string remotename, System.IO.Stream stream)
@@ -346,6 +346,12 @@ namespace Duplicati.Library.Backend.GoogleDrive
}
}
}
public string[] DNSName
{
get { return new string[] { new System.Uri(DRIVE_API_URL).Host, new System.Uri(DRIVE_API_UPLOAD_URL).Host }; }
}
#endregion
#region IRenameEnabledBackend implementation
+185 -6
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
@@ -14,13 +14,192 @@
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System;
using Duplicati.Library.Interface;
using Duplicati.Library;
using Duplicati.Library.Backend.OpenStack;
using System.Collections.Generic;
namespace Duplicati.Library.Backend.HubiC
{
public class HubiCBackend : IBackend, IStreamingBackend
namespace Duplicati.Library.Backend.HubiC
{
public class HubiCBackend : IBackend, IStreamingBackend
{
private const string AUTHID_OPTION = "authid";
private const string HUBIC_API_URL = "https://api.hubic.com/1.0/";
private const string HUBIC_API_CREDENTIAL_URL = HUBIC_API_URL + "account/credentials";
private OpenStackHelper m_openstack;
private class HubiCAuthResponse
{
public string token { get; set; }
public string endpoint { get; set; }
public DateTime? expires { get; set;}
}
private class OpenStackHelper : OpenStackStorage
{
private OAuthHelper m_helper;
private HubiCAuthResponse m_token;
public OpenStackHelper(string authid, string url)
: base(url, MockOptions())
{
m_helper = new OAuthHelper(authid, "hubic") { AutoAuthHeader = true };
}
private static Dictionary<string, string> MockOptions()
{
var res = new Dictionary<string, string>();
res["openstack-authuri"] = "invalid://dont-use";
res["openstack-apikey"] = "invalid";
res["auth-username"] = "invalid";
return res;
}
private HubiCAuthResponse AuthToken
{
get
{
if (m_token == null || (m_token.expires != null && (m_token.expires.Value - DateTime.UtcNow).TotalSeconds < 30))
m_token = m_helper.ReadJSONResponse<HubiCAuthResponse>(HUBIC_API_CREDENTIAL_URL);
return m_token;
}
}
protected override string AccessToken
{
get { return AuthToken.token; }
}
protected override string SimpleStorageEndPoint
{
get { return AuthToken.endpoint; }
}
public string EndPointDnsName
{
get
{
if (m_token == null || string.IsNullOrWhiteSpace(m_token.endpoint))
return null;
return new Uri(m_token.endpoint).Host;
}
}
}
public HubiCBackend()
{
}
public HubiCBackend(string url, Dictionary<string, string> options)
{
string authid = null;
if (options.ContainsKey(AUTHID_OPTION))
authid = options[AUTHID_OPTION];
m_openstack = new OpenStackHelper(authid, url);
}
#region IStreamingBackend implementation
public void Put(string remotename, System.IO.Stream stream)
{
m_openstack.Put(remotename, stream);
}
public void Get(string remotename, System.IO.Stream stream)
{
m_openstack.Get(remotename, stream);
}
#endregion
#region IBackend implementation
public IEnumerable<IFileEntry> List()
{
return m_openstack.List();
}
public void Put(string remotename, string filename)
{
m_openstack.Put(remotename, filename);
}
public void Get(string remotename, string filename)
{
m_openstack.Get(remotename, filename);
}
public void Delete(string remotename)
{
m_openstack.Delete(remotename);
}
public void Test()
{
m_openstack.Test();
}
public void CreateFolder()
{
m_openstack.CreateFolder();
}
public string DisplayName
{
get
{
return Strings.HubiC.DisplayName;
}
}
public string ProtocolKey
{
get
{
return "hubic";
}
}
public System.Collections.Generic.IList<ICommandLineArgument> SupportedCommands
{
get {
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.HubiC.AuthidShort, Strings.HubiC.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("hubic"))),
});
}
}
public string Description
{
get
{
return Strings.HubiC.Description;
}
}
public string[] DNSName
{
get { return new string[] { new Uri(HUBIC_API_URL).Host, m_openstack.EndPointDnsName }; }
}
#endregion
#region IDisposable implementation
public void Dispose()
{
if (m_openstack != null)
{
m_openstack.Dispose();
m_openstack = null;
}
}
#endregion
}
}
@@ -316,8 +316,13 @@ namespace Duplicati.Library.Backend
public bool SupportsStreaming
{
get { return true; }
}
}
public string[] DNSName
{
get { return new string[] { new Uri(JFS_ROOT).Host, new Uri(JFS_ROOT_UPLOAD).Host }; }
}
public void Get(string remotename, System.IO.Stream stream)
{
// Downloading from Jottacloud: Will only succeed if the file has a completed revision,
@@ -260,6 +260,11 @@ namespace Duplicati.Library.Backend.Mega
}
}
public string[] DNSName
{
get { return null; }
}
#endregion
#region IDisposable implementation
@@ -500,7 +500,12 @@ namespace Duplicati.Library.Backend
}
}
#endregion
public string[] DNSName
{
get { return new string[] { new Uri(WLID_SERVER).Host, new Uri(ONEDRIVE_SERVICE_URL).Host, string.IsNullOrWhiteSpace(m_userid) ? null : string.Format("cid-{0}.users.storage.live.com", m_userid) }; }
}
#endregion
#region IStreamingBackend Members
@@ -422,6 +422,18 @@ namespace Duplicati.Library.Backend.OpenStack
return Strings.OpenStack.Description;
}
}
public virtual string[] DNSName
{
get
{
return new string[] {
new System.Uri(m_authUri).Host,
string.IsNullOrWhiteSpace(m_simplestorageendpoint) ? null : new System.Uri(m_simplestorageendpoint).Host
};
}
}
#endregion
#region IDisposable implementation
public void Dispose()
+7 -2
View File
@@ -282,8 +282,13 @@ namespace Duplicati.Library.Backend
{
return Strings.Rclone.Description;
}
}
}
public string[] DNSName
{
get { return new string[] { remote_repo }; }
}
public void Test()
{
this.TestList();
@@ -474,6 +474,11 @@ namespace Duplicati.Library.Backend
get { return m_wrapper; }
}
public string[] DNSName
{
get { return new string[] { m_wrapper.DNSHost }; }
}
private string GetFullKey(string name)
{
//AWS SDK encodes the filenames correctly
@@ -38,6 +38,8 @@ namespace Duplicati.Library.Backend
protected string m_storageClass;
protected AmazonS3Client m_client;
public readonly string DNSHost;
public S3Wrapper(string awsID, string awsKey, string locationConstraint, string servername, string storageClass, bool useSSL, Dictionary<string, string> options)
{
var cfg = new AmazonS3Config();
@@ -73,6 +75,7 @@ namespace Duplicati.Library.Backend
m_locationConstraint = locationConstraint;
m_storageClass = storageClass;
DNSHost = string.IsNullOrWhiteSpace(cfg.ServiceURL) ? null : new Uri(cfg.ServiceURL).Host;
}
public void AddBucket(string bucketName)
@@ -368,5 +368,10 @@ namespace Duplicati.Library.Backend
return m_con;
}
}
public string[] DNSName
{
get { return new string[] { m_server }; }
}
}
}
@@ -113,12 +113,17 @@ namespace Duplicati.Library.Backend
new CommandLineArgument("chunk-size", CommandLineArgument.ArgumentType.Size, Strings.SharePoint.DescriptionChunkSizeShort, Strings.SharePoint.DescriptionChunkSizeLong, "4mb"),
});
}
}
}
public string[] DNSName
{
get { return new string[] { m_orgUrl.Host, string.IsNullOrWhiteSpace(m_spWebUrl) ? null : new Utility.Uri(m_spWebUrl).Host }; }
}
#endregion
#region [Constructors]
public SharePointBackend()
{ }
+9 -4
View File
@@ -438,12 +438,17 @@ namespace Duplicati.Library.Backend.Sia
{
return Strings.Sia.Description;
}
}
}
public string[] DNSName
{
get { return new string[] { new System.Uri(m_apihost).Host }; }
}
#endregion
#region IDisposable Members
public void Dispose()
{
@@ -254,6 +254,11 @@ namespace Duplicati.Library.Backend
get { return Strings.TahoeBackend.Description; }
}
public string[] DNSName
{
get { return new string[] { new Uri(m_url).Host }; }
}
#endregion
#region IDisposable Members
@@ -33,6 +33,7 @@ namespace Duplicati.Library.Backend
private string m_reverseProtocolUrl;
private string m_rawurl;
private string m_rawurlPort;
private string m_dnsName;
private bool m_useIntegratedAuthentication = false;
private bool m_forceDigestAuthentication = false;
private bool m_useSSL = false;
@@ -61,6 +62,7 @@ namespace Duplicati.Library.Backend
{
var u = new Utility.Uri(url);
u.RequireHost();
m_dnsName = u.Host;
if (!string.IsNullOrEmpty(u.Username))
{
@@ -311,6 +313,11 @@ namespace Duplicati.Library.Backend
get { return Strings.WEBDAV.Description; }
}
public string[] DNSName
{
get { return new string[] { m_dnsName }; }
}
public void Test()
{
this.List();
+2 -2
View File
@@ -16,8 +16,8 @@ namespace Duplicati.Library.Compression.Strings {
public static string NoWriterError { get { return LC.L(@"Archive not opened for writing"); } }
public static string NoReaderError { get { return LC.L(@"Archive not opened for reading"); } }
public static string FileNotFoundError { get { return LC.L(@"The given file is not part of this archive"); } }
public static string Description { get { return LC.L(@"7z Archive with LZMA2 support."); } }
public static string DisplayName { get { return LC.L(@"7z Archive"); } }
public static string Description { get { return LC.L(@"*Experimental*: 7z Archive with LZMA2 support."); } }
public static string DisplayName { get { return LC.L(@"Experimental - 7z Archive"); } }
public static string ThreadcountLong { get { return LC.L(@"The number of threads used in LZMA 2 compression. Defaults to the number of processor cores."); } }
public static string ThreadcountShort { get { return LC.L(@"Number of threads used in compression"); } }
public static string CompressionlevelLong { get { return LC.L(@"This option controls the compression level used. A setting of zero gives no compression, and a setting of 9 gives maximum compression."); } }
+5
View File
@@ -81,6 +81,11 @@ namespace Duplicati.Library.Interface
/// </summary>
string Description { get; }
/// <summary>
/// The DNS names used to resolve the IP addresses for this backend
/// </summary>
string[] DNSName { get; }
/// <summary>
/// The purpose of this method is to test the connection to the remote backend.
/// If any problem is encountered, this method should throw an exception.
+31 -4
View File
@@ -904,9 +904,31 @@ namespace Duplicati.Library.Main
/// <param name="log">The log instance</param>
private void ValidateOptions(ILogWriter log)
{
if (m_options.KeepTime.Ticks > 0 && m_options.KeepVersions > 0)
throw new Interface.UserInformationException(string.Format("Setting both --{0} and --{1} is not permitted", "keep-versions", "keep-time"));
// Check if only one of the retention options is set
var selectedRetentionOptions = new List<String>();
if (m_options.KeepTime.Ticks > 0)
{
selectedRetentionOptions.Add("keep-time");
}
if (m_options.KeepVersions > 0)
{
selectedRetentionOptions.Add("keep-versions");
}
if (m_options.RetentionPolicy.Count() > 0)
{
selectedRetentionOptions.Add("retention-policy");
}
if (selectedRetentionOptions.Count() > 1)
{
throw new Interface.UserInformationException(string.Format("Setting multiple retention options ({0}) is not permitted",
String.Join(", ", selectedRetentionOptions.Select(x => "--" + x))));
}
// Check Prefix
if (!string.IsNullOrWhiteSpace(m_options.Prefix) && m_options.Prefix.Contains("-"))
throw new Interface.UserInformationException("The prefix cannot contain hyphens (-)");
@@ -915,9 +937,10 @@ namespace Duplicati.Library.Main
{
foreach (var configEntry in m_options.RetentionPolicy)
{
if (configEntry.Value >= configEntry.Key)
if (!configEntry.IsKeepAllVersions() && !configEntry.IsUnlimtedTimeframe() &&
configEntry.Interval >= configEntry.Timeframe)
{
throw new Interface.UserInformationException("A time frame cannot be smaller than its interval");
throw new Interface.UserInformationException("An interval cannot be bigger than the timeframe it is in");
}
}
}
@@ -1030,6 +1053,10 @@ namespace Duplicati.Library.Main
}
}
// For now, warn not to use 7z
if (string.Equals(m_options.CompressionModule, "7z", StringComparison.OrdinalIgnoreCase))
log.AddWarning("The 7z compression module has known issues and should only be used for experimental purposes", null);
//TODO: Based on the action, see if all options are relevant
}
@@ -608,6 +608,18 @@ namespace Duplicati.Library.Main.Database
return lastFilesetId;
}
internal Tuple<long, long> GetLastBackupFileCountAndSize()
{
using (var cmd = m_connection.CreateCommand())
{
var lastFilesetId = cmd.ExecuteScalarInt64(@"SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC LIMIT 1");
var count = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""File"" INNER JOIN ""FilesetEntry"" ON ""File"".""ID"" = ""FilesetEntry"".""FileID"" WHERE ""FilesetEntry"".""FilesetID"" = ? AND ""File"".""BlocksetID"" NOT IN (?, ?)", -1, lastFilesetId, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID);
var size = cmd.ExecuteScalarInt64(@"SELECT SUM(""Blockset"".""Length"") FROM ""File"", ""FilesetEntry"", ""Blockset"" WHERE ""File"".""ID"" = ""FilesetEntry"".""FileID"" AND ""File"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FilesetEntry"".""FilesetID"" = ? AND ""File"".""BlocksetID"" NOT IN (?, ?)", -1, lastFilesetId, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID);
return new Tuple<long, long>(count, size);
}
}
internal void UpdateChangeStatistics(BackupResults results)
{
using(var cmd = m_connection.CreateCommand(Transaction))
@@ -21,6 +21,7 @@ using System.IO;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Main.Operation.Common;
using Duplicati.Library.Snapshots;
namespace Duplicati.Library.Main.Operation.Backup
{
@@ -191,7 +192,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
// If the file is a symlink, apply special handling
var isSymlink = (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
var isSymlink = snapshot.IsSymlink(path, attributes);
if (isSymlink && symlinkPolicy == Options.SymlinkStrategy.Ignore)
{
await log.WriteVerboseAsync("Excluding symlink: {0}", path);
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
using System.Collections.Generic;
using CoCoL;
using Duplicati.Library.Main.Operation.Common;
using Duplicati.Library.Snapshots;
namespace Duplicati.Library.Main.Operation.Backup
{
@@ -35,7 +36,7 @@ namespace Duplicati.Library.Main.Operation.Backup
if (options.StoreMetadata)
{
metadata = snapshot.GetMetadata(path, attributes.HasFlag(System.IO.FileAttributes.ReparsePoint), options.SymlinkPolicy == Options.SymlinkStrategy.Follow);
metadata = snapshot.GetMetadata(path, snapshot.IsSymlink(path, attributes), options.SymlinkPolicy == Options.SymlinkStrategy.Follow);
if (metadata == null)
metadata = new Dictionary<string, string>();
@@ -143,33 +143,46 @@ namespace Duplicati.Library.Main.Operation.Backup
{
if ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
if (options.SymlinkPolicy == Options.SymlinkStrategy.Ignore)
// Not all reparse points are symlinks.
// For example, on Windows 10 Fall Creator's Update, the OneDrive folder (and all subfolders)
// are reparse points, which allows the folder to hook into the OneDrive service and download things on-demand.
// If we can't find a symlink target for the current path, we won't treat it as a symlink.
string symlinkTarget = snapshot.GetSymlinkTarget(path);
if (!string.IsNullOrWhiteSpace(symlinkTarget))
{
await log.WriteVerboseAsync("Ignoring symlink {0}", path);
return false;
}
if (options.SymlinkPolicy == Options.SymlinkStrategy.Store)
{
var metadata = await MetadataGenerator.GenerateMetadataAsync(path, attributes, options, snapshot, log);
if (!metadata.ContainsKey("CoreSymlinkTarget"))
if (options.SymlinkPolicy == Options.SymlinkStrategy.Ignore)
{
var p = snapshot.GetSymlinkTarget(path);
if (string.IsNullOrWhiteSpace(p))
await log.WriteVerboseAsync("Ignoring empty symlink {0}", path);
else
metadata["CoreSymlinkTarget"] = p;
await log.WriteVerboseAsync("Ignoring symlink {0}", path);
return false;
}
var metahash = Utility.WrapMetadata(metadata, options);
await AddSymlinkToOutputAsync(path, DateTime.UtcNow, metahash, log, database, streamblockchannel);
if (options.SymlinkPolicy == Options.SymlinkStrategy.Store)
{
var metadata = await MetadataGenerator.GenerateMetadataAsync(path, attributes, options, snapshot, log);
await log.WriteVerboseAsync("Stored symlink {0}", path);
// Don't process further
return false;
if (!metadata.ContainsKey("CoreSymlinkTarget"))
{
var p = snapshot.GetSymlinkTarget(path);
if (string.IsNullOrWhiteSpace(p))
await log.WriteVerboseAsync("Ignoring empty symlink {0}", path);
else
metadata["CoreSymlinkTarget"] = p;
}
var metahash = Utility.WrapMetadata(metadata, options);
await AddSymlinkToOutputAsync(path, DateTime.UtcNow, metahash, log, database, streamblockchannel);
await log.WriteVerboseAsync("Stored symlink {0}", path);
// Don't process further
return false;
}
}
else
{
await log.WriteVerboseAsync("Treating empty symlink as regular path {0}", path);
}
}
if ((attributes & FileAttributes.Directory) == FileAttributes.Directory)
@@ -300,8 +300,20 @@ namespace Duplicati.Library.Main.Operation
{
try
{
// Start the parallel scanner
parallelScanner = Backup.CountFilesHandler.Run(snapshot, m_result, m_options, m_sourceFilter, m_filter, m_result.TaskReader, counterToken.Token);
// Start parallel scan, or use the database
if (m_options.ChangedFilelist == null || m_options.ChangedFilelist.Length < 1)
{
if (m_options.DisableFileScanner)
{
var d = m_database.GetLastBackupFileCountAndSize();
m_result.OperationProgressUpdater.UpdatefileCount(d.Item1, d.Item2, true);
}
else
{
parallelScanner = Backup.CountFilesHandler.Run(snapshot, m_result, m_options, m_sourceFilter, m_filter, m_result.TaskReader, counterToken.Token);
}
}
// Make sure the database is sane
await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, true);
@@ -375,6 +375,28 @@ namespace Duplicati.Library.Main.Operation.Common
if (ex is System.Threading.ThreadAbortException || ex is OperationCanceledException)
break;
if (ex is System.Net.WebException && ((System.Net.WebException)ex).Status == System.Net.WebExceptionStatus.NameResolutionFailure)
{
try
{
var names = m_backend.DNSName ?? new string[0];
foreach (var name in names)
if (!string.IsNullOrWhiteSpace(name))
try
{
System.Net.Dns.GetHostEntry(name);
}
catch (Exception dnsex)
{
await m_log.WriteVerboseAsync("Failed to refresh DNS record for {0}: {1}", name, dnsex);
}
}
catch (Exception bkdnsex)
{
await m_log.WriteWarningAsync("Failed to get DNS names from the backend", bkdnsex);
}
}
await m_stats.SendEventAsync(item.Operation, i < m_options.NumberOfRetries ? BackendEventType.Retrying : BackendEventType.Failed, item.RemoteFilename, item.Size);
bool recovered = false;
@@ -195,8 +195,8 @@ namespace Duplicati.Library.Main.Operation
private List<DateTime> ApplyRetentionPolicy(DateTime[] backups)
{
// Any work to do?
Dictionary<TimeSpan, TimeSpan> retentionPolicyOptionValue = m_options.RetentionPolicy;
if (retentionPolicyOptionValue.Count == 0 || backups.Length == 0)
var retentionPolicyOptionValues = m_options.RetentionPolicy;
if (retentionPolicyOptionValues.Count == 0 || backups.Length == 0)
{
return new List<DateTime>(); // don't delete any backups
}
@@ -209,11 +209,14 @@ namespace Duplicati.Library.Main.Operation
// Make sure the backups are in descending order (newest backup in the beginning)
clonedBackupList = clonedBackupList.OrderByDescending(x => x).ToList();
// Most current backup should never get deleted in this process, so exclude it
// Most recent backup usually should never get deleted in this process, so exclude it for now,
// but keep a reference to potentiall delete it when allow-full-removal is set
var mostRecentBackup = clonedBackupList.ElementAt(0);
clonedBackupList.RemoveAt(0);
var deleteMostRecentBackup = m_options.AllowFullRemoval;
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Time frames and intervals pairs: {0}",
string.Join(", ", retentionPolicyOptionValue.Select(x => x.Key + " / " + x.Value))), Logging.LogMessageType.Information);
string.Join(", ", retentionPolicyOptionValues)), Logging.LogMessageType.Information);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Backups to consider: {0}",
string.Join(", ", clonedBackupList)), Logging.LogMessageType.Information);
@@ -221,18 +224,14 @@ namespace Duplicati.Library.Main.Operation
// Collect all potential backups in each time frame and thin out according to the specified interval,
// starting with the oldest backup in that time frame.
// The order in which the time frames values are checked has to be from the smallest to the largest.
// If backups are not within any time frame, they will NOT be deleted here.
// The --keep-time and --keep-versions switched should be used to ultimately delete backups that are too old
List<DateTime> backupsToDelete = new List<DateTime>();
var now = DateTime.Now;
foreach (var singleRetentionPolicyOptionValue in retentionPolicyOptionValue.OrderBy(x => x.Key))
foreach (var singleRetentionPolicyOptionValue in retentionPolicyOptionValues.OrderBy(x => x.Timeframe))
{
var period = singleRetentionPolicyOptionValue.Key;
var interval = singleRetentionPolicyOptionValue.Value;
// The timeframe in the retention policy option is only a timespan which has to be applied to the current DateTime to get the actual lower bound
DateTime timeFrame = (singleRetentionPolicyOptionValue.IsUnlimtedTimeframe()) ? DateTime.MinValue : (now - singleRetentionPolicyOptionValue.Timeframe);
DateTime timeFrame = (period > TimeSpan.Zero) ? (now - period) : DateTime.MinValue; // period equal or below 0 means "biggest time frame possible"
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Next time frame and interval pair: {0} / {1}", timeFrame, interval), Logging.LogMessageType.Profiling);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Next time frame and interval pair: {0}", singleRetentionPolicyOptionValue.ToString()), Logging.LogMessageType.Profiling);
List<DateTime> backupsInTimeFrame = new List<DateTime>();
while (clonedBackupList.Count > 0 && clonedBackupList[0] >= timeFrame)
@@ -251,23 +250,36 @@ namespace Duplicati.Library.Main.Operation
// Keep this backup if
// - no backup has yet been added to the time frame (keeps at least the oldest backup in a time frame)
// - difference between last added backup and this backup is bigger than the specified interval
if (lastKept == null || (backup - lastKept.Value) >= interval)
if (lastKept == null || singleRetentionPolicyOptionValue.IsKeepAllVersions() || (backup - lastKept.Value) >= singleRetentionPolicyOptionValue.Interval)
{
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Keeping backup: {0}", backup), Logging.LogMessageType.Profiling);
lastKept = backup;
}
else
{
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Marking backup for deletion: {0}", backup), Logging.LogMessageType.Profiling);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Deleting backup: {0}", backup), Logging.LogMessageType.Profiling);
backupsToDelete.Add(backup);
}
}
// Check if most recent backup is outside of this time frame (meaning older/smaller)
deleteMostRecentBackup &= (mostRecentBackup < timeFrame);
}
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Backups outside of all time frames and thus not checked: {0}",
string.Join(", ", clonedBackupList)), Logging.LogMessageType.Profiling);
// Delete all remaining backups
backupsToDelete.AddRange(clonedBackupList);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Backups outside of all time frames and thus getting deleted: {0}",
string.Join(", ", clonedBackupList)), Logging.LogMessageType.Information);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Backups to delete: {0}",
// Delete most recent backup if allow-full-removal is set and the most current backup is outside of any time frame
if (deleteMostRecentBackup)
{
backupsToDelete.Add(mostRecentBackup);
Logging.Log.WriteMessage(string.Format("[Retention Policy]: Deleting most recent backup: {0}",
mostRecentBackup), Logging.LogMessageType.Information);
}
Logging.Log.WriteMessage(string.Format("[Retention Policy]: All backups to delete: {0}",
string.Join(", ", backupsToDelete.OrderByDescending(x => x))), Logging.LogMessageType.Information);
return backupsToDelete;
@@ -208,7 +208,8 @@ namespace Duplicati.Library.Main.Operation
select n).ToList();
log.KnownFileCount = remotelist.Count;
log.KnownFileSize = remotelist.Select(x => Math.Max(0, x.File.Size)).Sum();
long knownFileSize = remotelist.Select(x => Math.Max(0, x.File.Size)).Sum();
log.KnownFileSize = knownFileSize;
log.UnknownFileCount = unknownlist.Count;
log.UnknownFileSize = unknownlist.Select(x => Math.Max(0, x.Size)).Sum();
log.BackupListCount = filesets.Count;
@@ -223,6 +224,27 @@ namespace Duplicati.Library.Main.Operation
{
log.TotalQuotaSpace = quota.TotalQuotaSpace;
log.FreeQuotaSpace = quota.FreeQuotaSpace;
// Check to see if there should be a warning or error about the quota
// Since this processor may be called multiple times during a backup
// (both at the start and end, for example), the log keeps track of
// whether a quota error or warning has been sent already.
// Note that an error can still be sent later even if a warning was sent earlier.
if (!log.ReportedQuotaError && quota.FreeQuotaSpace == 0)
{
log.ReportedQuotaError = true;
log.AddError(string.Format("Backend quota has been exceeded: Using {0} of {1} ({2} available)", Library.Utility.Utility.FormatSizeString(knownFileSize), Library.Utility.Utility.FormatSizeString(quota.TotalQuotaSpace), Library.Utility.Utility.FormatSizeString(quota.FreeQuotaSpace)), null);
}
else if (!log.ReportedQuotaWarning && !log.ReportedQuotaError && quota.FreeQuotaSpace >= 0) // Negative value means the backend didn't return the quota info
{
// Warnings are sent if the available free space is less than the given percentage of the total backup size.
double warningThreshold = options.QuotaWarningThreshold / (double)100;
if (quota.FreeQuotaSpace < warningThreshold * knownFileSize)
{
log.ReportedQuotaWarning = true;
log.AddWarning(string.Format("Backend quota is close to being exceeded: Using {0} of {1} ({2} available)", Library.Utility.Utility.FormatSizeString(knownFileSize), Library.Utility.Utility.FormatSizeString(quota.TotalQuotaSpace), Library.Utility.Utility.FormatSizeString(quota.FreeQuotaSpace)), null);
}
}
}
}
@@ -17,8 +17,7 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Snapshots;
namespace Duplicati.Library.Main.Operation
{
@@ -32,196 +31,6 @@ namespace Duplicati.Library.Main.Operation
m_options = options;
m_result = results;
}
public class FilterHandler
{
private Snapshots.ISnapshotService m_snapshot;
private FileAttributes m_attributeFilter;
private Duplicati.Library.Utility.IFilter m_enumeratefilter;
private Duplicati.Library.Utility.IFilter m_emitfilter;
private Options.SymlinkStrategy m_symlinkPolicy;
private Options.HardlinkStrategy m_hardlinkPolicy;
private ILogWriter m_logWriter;
private Dictionary<string, string> m_hardlinkmap;
private Duplicati.Library.Utility.IFilter m_sourcefilter;
private Queue<string> m_mixinqueue;
public FilterHandler(Snapshots.ISnapshotService snapshot, FileAttributes attributeFilter, Duplicati.Library.Utility.IFilter sourcefilter, Duplicati.Library.Utility.IFilter filter, Options.SymlinkStrategy symlinkPolicy, Options.HardlinkStrategy hardlinkPolicy, ILogWriter logWriter)
{
m_snapshot = snapshot;
m_attributeFilter = attributeFilter;
m_sourcefilter = sourcefilter;
m_emitfilter = filter;
m_symlinkPolicy = symlinkPolicy;
m_hardlinkPolicy = hardlinkPolicy;
m_logWriter = logWriter;
m_hardlinkmap = new Dictionary<string, string>();
m_mixinqueue = new Queue<string>();
bool includes;
bool excludes;
Library.Utility.FilterExpression.AnalyzeFilters(filter, out includes, out excludes);
if (includes && !excludes)
{
m_enumeratefilter = Library.Utility.FilterExpression.Combine(filter, new Duplicati.Library.Utility.FilterExpression("*" + System.IO.Path.DirectorySeparatorChar, true));
}
else
m_enumeratefilter = m_emitfilter;
}
public void ReportError(string rootpath, string path, Exception ex)
{
if (m_logWriter != null)
m_logWriter.AddWarning(string.Format("Error reported while accessing file: {0}", path), ex);
}
public bool AttributeFilter(string rootpath, string path, FileAttributes attributes)
{
try
{
if (m_snapshot.IsBlockDevice(path))
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding block device: {0}", path);
return false;
}
}
catch (Exception ex)
{
if (m_logWriter != null)
m_logWriter.AddWarning(string.Format("Failed to process path: {0}", path), ex);
return false;
}
Duplicati.Library.Utility.IFilter sourcematch;
bool sourcematches;
if (m_sourcefilter.Matches(path, out sourcematches, out sourcematch) && sourcematches)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Including source path: {0}", path);
return true;
}
if (m_hardlinkPolicy != Options.HardlinkStrategy.All)
{
try
{
var id = m_snapshot.HardlinkTargetID(path);
if (id != null)
{
if (m_hardlinkPolicy == Options.HardlinkStrategy.None)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding hardlink: {0} ({1})", path, id);
return false;
}
else if (m_hardlinkPolicy == Options.HardlinkStrategy.First)
{
string prevPath;
if (m_hardlinkmap.TryGetValue(id, out prevPath))
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding hardlink ({1}) for: {0}, previous hardlink: {2}", path, id, prevPath);
return false;
}
else
{
m_hardlinkmap.Add(id, path);
}
}
}
}
catch (Exception ex)
{
if (m_logWriter != null)
m_logWriter.AddWarning(string.Format("Failed to process path: {0}", path), ex);
return false;
}
}
if ((m_attributeFilter & attributes) != 0)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding path due to attribute filter: {0}", path);
return false;
}
Library.Utility.IFilter match;
if (!Library.Utility.FilterExpression.Matches(m_enumeratefilter, path, out match))
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding path due to filter: {0} => {1}", path, match == null ? "null" : match.ToString());
return false;
}
else if (match != null)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Including path due to filter: {0} => {1}", path, match.ToString());
}
var isSymlink = (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
if (isSymlink && m_symlinkPolicy == Options.SymlinkStrategy.Ignore)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Excluding symlink: {0}", path);
return false;
}
if (isSymlink && m_symlinkPolicy == Options.SymlinkStrategy.Store)
{
if (m_logWriter != null)
m_logWriter.AddVerboseMessage("Storing symlink: {0}", path);
m_mixinqueue.Enqueue(path);
return false;
}
return true;
}
public IEnumerable<string> EnumerateFilesAndFolders()
{
foreach(var s in m_snapshot.EnumerateFilesAndFolders(this.AttributeFilter, ReportError))
{
while (m_mixinqueue.Count > 0)
yield return m_mixinqueue.Dequeue();
Library.Utility.IFilter m;
if (m_emitfilter != m_enumeratefilter && !Library.Utility.FilterExpression.Matches(m_emitfilter, s, out m))
continue;
yield return s;
}
while (m_mixinqueue.Count > 0)
yield return m_mixinqueue.Dequeue();
}
public IEnumerable<string> Mixin(IEnumerable<string> list)
{
foreach(var s in list.Where(x => {
var fa = FileAttributes.Normal;
try { fa = m_snapshot.GetAttributes(x); }
catch { }
return AttributeFilter(null, x, fa);
}))
{
while (m_mixinqueue.Count > 0)
yield return m_mixinqueue.Dequeue();
yield return s;
}
while (m_mixinqueue.Count > 0)
yield return m_mixinqueue.Dequeue();
}
}
public void Run(string[] sources, Library.Utility.IFilter filter)
{
@@ -230,13 +39,13 @@ namespace Duplicati.Library.Main.Operation
using(var snapshot = BackupHandler.GetSnapshot(sources, m_options, m_result))
{
foreach(var path in new FilterHandler(snapshot, m_options.FileAttributeFilter, sourcefilter, filter, m_options.SymlinkPolicy, m_options.HardlinkPolicy, m_result).EnumerateFilesAndFolders())
foreach(var path in new BackupHandler.FilterHandler(snapshot, m_options.FileAttributeFilter, sourcefilter, filter, m_options.SymlinkPolicy, m_options.HardlinkPolicy, m_result).EnumerateFilesAndFolders())
{
var fa = FileAttributes.Normal;
try { fa = snapshot.GetAttributes(path); }
catch { }
if (storeSymlinks && ((fa & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint))
if (storeSymlinks && snapshot.IsSymlink(path, fa))
{
m_result.AddVerboseMessage("Storing symlink: {0}", path);
}
+128 -9
View File
@@ -84,6 +84,10 @@ namespace Duplicati.Library.Main
/// </summary>
private readonly int DEFAULT_BLOCK_HASHERS = Math.Max(1, Environment.ProcessorCount / 2);
/// The default threshold for warning about coming close to quota
/// </summary>
private const int DEFAULT_QUOTA_WARNING_THRESHOLD = 10;
/// <summary>
/// An enumeration that describes the supported strategies for an optimization
/// </summary>
@@ -261,7 +265,8 @@ namespace Duplicati.Library.Main
"exclude-files-attributes",
"compression-extension-file",
"full-remote-verification",
"disable-synthetic-filelist"
"disable-synthetic-filelist",
"disable-file-scanner"
};
}
}
@@ -470,8 +475,9 @@ namespace Duplicati.Library.Main
new CommandLineArgument("list-verify-uploads", CommandLineArgument.ArgumentType.Boolean, Strings.Options.ListverifyuploadsShort, Strings.Options.ListverifyuploadsShort, "false"),
new CommandLineArgument("allow-sleep", CommandLineArgument.ArgumentType.Boolean, Strings.Options.AllowsleepShort, Strings.Options.AllowsleepLong, "false"),
new CommandLineArgument("no-connection-reuse", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NoconnectionreuseShort, Strings.Options.NoconnectionreuseLong, "false"),
new CommandLineArgument("quota-size", CommandLineArgument.ArgumentType.Size, Strings.Options.QuotasizeShort, Strings.Options.QuotasizeLong),
new CommandLineArgument("quota-warning-threshold", CommandLineArgument.ArgumentType.Integer, Strings.Options.QuotaWarningThresholdShort, Strings.Options.QuotaWarningThresholdLong, DEFAULT_QUOTA_WARNING_THRESHOLD.ToString()),
new CommandLineArgument("default-filters", CommandLineArgument.ArgumentType.String, Strings.Options.DefaultFiltersShort, Strings.Options.DefaultFiltersLong(DefaultFilterSet.Windows.ToString(), DefaultFilterSet.OSX.ToString(), DefaultFilterSet.Linux.ToString(), DefaultFilterSet.All.ToString()), string.Empty, new[] { "default-filter" }),
@@ -538,6 +544,7 @@ namespace Duplicati.Library.Main
new CommandLineArgument("concurrency-compressors", CommandLineArgument.ArgumentType.Integer, Strings.Options.ConcurrencycompressorsShort, Strings.Options.ConcurrencycompressorsLong, DEFAULT_COMPRESSORS.ToString()),
new CommandLineArgument("auto-vacuum", CommandLineArgument.ArgumentType.Boolean, Strings.Options.AutoVacuumShort, Strings.Options.AutoVacuumLong, "false"),
new CommandLineArgument("disable-file-scanner", CommandLineArgument.ArgumentType.Boolean, Strings.Options.DisablefilescannerShort, Strings.Options.DisablefilescannerLong, "false"),
});
return lst;
@@ -822,10 +829,10 @@ namespace Duplicati.Library.Main
/// <summary>
/// Gets the time frames and intervals for the retention policy
/// </summary>
public Dictionary<TimeSpan, TimeSpan> RetentionPolicy
public List<RetentionPolicyValue> RetentionPolicy
{
get {
var retentionPolicyConfig = new Dictionary<TimeSpan, TimeSpan>();
var retentionPolicyConfig = new List<RetentionPolicyValue>();
string v;
m_options.TryGetValue("retention-policy", out v);
@@ -837,11 +844,7 @@ namespace Duplicati.Library.Main
foreach (var periodIntervalString in periodIntervalStrings)
{
var periodInterval = periodIntervalString.Split(':');
var period = Library.Utility.Timeparser.ParseTimeSpan(periodInterval[0]);
var interval = Library.Utility.Timeparser.ParseTimeSpan(periodInterval[1]);
retentionPolicyConfig.Add(period, interval);
retentionPolicyConfig.Add(RetentionPolicyValue.CreateFromString(periodIntervalString));
}
return retentionPolicyConfig;
@@ -1317,6 +1320,29 @@ namespace Duplicati.Library.Main
}
}
/// <summary>
/// Gets the threshold at which a quota warning should be generated.
/// </summary>
/// <remarks>
/// This is treated as a percentage, where a warning is given when the amount of free space is less than this percentage of the backup size.
/// </remarks>
public int QuotaWarningThreshold
{
get
{
string tmp;
m_options.TryGetValue("quota-warning-threshold", out tmp);
if (string.IsNullOrEmpty(tmp))
{
return DEFAULT_QUOTA_WARNING_THRESHOLD;
}
else
{
return int.Parse(tmp);
}
}
}
/// <summary>
/// Gets the display name of the backup
/// </summary>
@@ -1776,6 +1802,15 @@ namespace Duplicati.Library.Main
get { return GetBool("auto-vacuum"); }
}
/// <summary>
/// Gets a flag indicating if the local filescanner should be disabled
/// </summary>
/// <value><c>true</c> if the filescanner should be disabled; otherwise, <c>false</c>.</value>
public bool DisableFileScanner
{
get { return Library.Utility.Utility.ParseBoolOption(m_options, "disable-file-scanner"); }
}
/// <summary>
/// Gets the threshold for when log data should be cleaned
/// </summary>
@@ -1910,5 +1945,89 @@ namespace Duplicati.Library.Main
return Library.Utility.Utility.ParseBoolOption(m_options, name);
}
/// <summary>
/// Class for handling a single RententionPolicy timeframe-interval-pair
/// </summary>
public class RetentionPolicyValue
{
public readonly TimeSpan Timeframe;
public readonly TimeSpan Interval;
public RetentionPolicyValue(TimeSpan timeframe, TimeSpan interval)
{
if (timeframe < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeframe), string.Format("The timeframe cannot be negative: '{0}'", timeframe));
}
if (interval < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(interval), string.Format("The interval cannot be negative: '{0}'", interval));
}
this.Timeframe = timeframe;
this.Interval = interval;
}
/// <summary>
/// Returns whether this is an unlimited timeframe or not
/// </summary>
/// <returns></returns>
public Boolean IsUnlimtedTimeframe()
{
// Timeframes equal or bigger than the maximum TimeSpan effectivly represent an unlimited timeframe
return Timeframe >= TimeSpan.MaxValue;
}
/// <summary>
/// Returns whether all versions in this timeframe should be kept or not
/// </summary>
/// <returns></returns>
public Boolean IsKeepAllVersions()
{
/// Intervals between two versions that are equal or smaller than zero effectivly result in
/// all versions in that timeframe being kept.
return Interval <= TimeSpan.Zero;
}
public override string ToString()
{
return (IsUnlimtedTimeframe() ? "Unlimited" : Timeframe.ToString()) + " / " + (IsKeepAllVersions() ? "Keep all" : Interval.ToString());
}
/// <summary>
/// Parses a string representation of a timeframe-interval-pair and returns a RentionPolicyValue object
/// </summary>
/// <returns></returns>
public static RetentionPolicyValue CreateFromString(string rententionPolicyValueString)
{
var periodInterval = rententionPolicyValueString.Split(':');
TimeSpan timeframe;
// Timeframe "U" (= Unlimited) means: For unlimted time keep one version every X interval.
// So the timeframe has to span the maximum time possible.
if (String.Equals(periodInterval[0], "U", StringComparison.OrdinalIgnoreCase))
{
timeframe = TimeSpan.MaxValue;
}
else
{
timeframe = Library.Utility.Timeparser.ParseTimeSpan(periodInterval[0]);
}
TimeSpan interval;
// Interval "U" (= Unlimited) means: For period X keep all versions.
// So the interval between two versions has to be zero.
if (String.Equals(periodInterval[1], "U", StringComparison.OrdinalIgnoreCase))
{
interval = TimeSpan.Zero;
}
else
{
interval = Library.Utility.Timeparser.ParseTimeSpan(periodInterval[1]);
}
return new RetentionPolicyValue(timeframe, interval);
}
}
}
}
+6
View File
@@ -49,6 +49,9 @@ namespace Duplicati.Library.Main
long FreeQuotaSpace { set; }
long AssignedQuotaSpace { set; }
bool ReportedQuotaError { get; set; }
bool ReportedQuotaWarning { get; set; }
/// <summary>
/// The backend sends this event when performing an action
/// </summary>
@@ -126,6 +129,9 @@ namespace Duplicati.Library.Main
public long FreeQuotaSpace { get; set; }
public long AssignedQuotaSpace { get; set; }
public bool ReportedQuotaError { get; set; }
public bool ReportedQuotaWarning { get; set; }
public override OperationMode MainOperation { get { return m_parent.MainOperation; } }
public void SendEvent(BackendActionType action, BackendEventType type, string path, long size)
+5 -1
View File
@@ -119,6 +119,8 @@ namespace Duplicati.Library.Main.Strings
public static string UploadUnchangedBackupsShort { get { return LC.L(@"Upload empty backup files"); } }
public static string QuotasizeLong { get { return LC.L(@"This value can be used to set a known upper limit on the amount of space a backend has. If the backend reports the size itself, this value is ignored"); } }
public static string QuotasizeShort { get { return LC.L(@"A reported maximum storage"); } }
public static string QuotaWarningThresholdLong { get { return LC.L(@"Sets a threshold for when to warn about the backend quota being nearly exceeded. It is given as a percentage, and a warning is generated if the amount of available quota is less that this percentage of the total backup size. If the backend does not report the quota information, this value will be ignored"); } }
public static string QuotaWarningThresholdShort { get { return LC.L(@"Threshold for warning about low quota"); } }
public static string DefaultFiltersLong(string windows, string osx, string linux, string all) { return LC.L(@"Exclude files that match the given filter sets. Which default filter sets should be used. Valid sets are ""{0}"", ""{1}"", ""{2}"", and ""{3}"". If this parameter is set with no value, the set for the current operating system will be used.", windows, osx, linux, all); }
public static string DefaultFiltersShort { get { return LC.L(@"Default filter sets"); } }
public static string SymlinkpolicyShort { get { return LC.L(@"Symlink handling"); } }
@@ -181,7 +183,7 @@ namespace Duplicati.Library.Main.Strings
public static string KeeptimeShort { get { return LC.L(@"Keep all versions within a timespan"); } }
public static string KeeptimeLong { get { return LC.L(@"Use this option to set the timespan in which backups are kept."); } }
public static string RetentionPolicyShort { get { return LC.L(@"Reduce number of versions by deleting old intermediate backups"); } }
public static string RetentionPolicyLong { get { return LC.L(@"Use this option to reduce the number of versions that are kept with increasing version age by deleting most of the old backups. The expected format is a comma seperated list of collon sperated time frame and interval pairs. For example the value ""7D:0s,3M:1D,10Y:2M"" means ""For 7 day keep all backups, for 3 months keep one backup per day and for 10 years one backup every 2nd month"); } }
public static string RetentionPolicyLong { get { return LC.L(@"Use this option to reduce the number of versions that are kept with increasing version age by deleting most of the old backups. The expected format is a comma separated list of colon separated time frame and interval pairs. For example the value ""7D:0s,3M:1D,10Y:2M"" means ""For 7 day keep all backups, for 3 months keep one backup every day, for 10 years one backup every 2nd month and delete every backup older than this."". This option also supports using the specifier ""U"" to indicate an unlimited time interval."); } }
public static string AllowmissingsourceShort { get { return LC.L(@"Ignore missing source elements"); } }
public static string AllowmissingsourceLong { get { return LC.L(@"Use this option to continue even if some source entries are missing."); } }
public static string OverwriteShort { get { return LC.L(@"Overwrite files when restoring"); } }
@@ -241,6 +243,8 @@ namespace Duplicati.Library.Main.Strings
public static string AllowfullremovalLong { get { return LC.L(@"By default, the last fileset cannot be removed. This is a safeguard to make sure that all remote data is not deleted by a configuration mistake. Use this flag to disable that protection, such that all filesets can be deleted."); } }
public static string AutoVacuumShort { get { return LC.L(@"Allow automatic rebuilding of local database to save space."); } }
public static string AutoVacuumLong { get { return LC.L(@"Some operations that manipulate the local database leave unused entries behind. These entries are not deleted from a hard drive until a VACUUM operation is run. This operation saves disk space in the long run but needs to temporarily create a copy of all valid entries in the database. Setting this to true will allow Duplicati to perform VACUUM operations at its discretion."); } }
public static string DisablefilescannerShort { get { return LC.L(@"Disable the read-ahead scanner"); } }
public static string DisablefilescannerLong { get { return LC.L(@"When this flag is enabled, the scanner that computes the size of source files is disabled, and instead the reported size is read from the database. Using this flag can speed up the backup by reducing disk access, but will give a less accurate progress indicator."); } }
}
internal static class Common
@@ -82,6 +82,7 @@
.webm #WebM Video File
.wmv #Windows Media Video File
.wtv #Windows Recorded TV Show
.m2ts # Blu-ray Disc MPEG-2 Transport Stream
# Compressed image files
+1
View File
@@ -50,6 +50,7 @@ namespace Duplicati.Library.Snapshots
FileAttributes GetFileAttributes(string path);
void SetFileAttributes(string path, FileAttributes attributes);
void CreateSymlink(string symlinkfile, string target, bool asDir);
string GetSymlinkTarget(string path);
string PathGetDirectoryName(string path);
string PathGetFileName(string path);
string PathGetExtension(string path);
+1 -1
View File
@@ -455,7 +455,7 @@ namespace Duplicati.Library.Snapshots
public string GetSymlinkTarget(string file)
{
var local = ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file);
return UnixSupport.File.GetSymlinkTarget(NoSnapshot.NormalizePath(local));
return _sysIO.GetSymlinkTarget(local);
}
/// <summary>
@@ -45,7 +45,7 @@ namespace Duplicati.Library.Snapshots
/// <returns>The symlink target</returns>
public override string GetSymlinkTarget(string file)
{
return UnixSupport.File.GetSymlinkTarget(NormalizePath(file));
return _sysIO.GetSymlinkTarget(file);
}
/// <summary>
@@ -46,11 +46,7 @@ namespace Duplicati.Library.Snapshots
/// <returns>The symlink target</returns>
public override string GetSymlinkTarget(string file)
{
try { return File.GetLinkTargetInfo(SystemIOWindows.PrefixWithUNC(file)).PrintName; }
catch (NotAReparsePointException) { }
catch (UnrecognizedReparsePointException) { }
return null;
return m_sysIO.GetSymlinkTarget(file);
}
/// <summary>
@@ -19,6 +19,7 @@
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Duplicati.Library.Snapshots
@@ -71,6 +72,60 @@ namespace Duplicati.Library.Snapshots
return new WindowsSnapshot(folders, options);
}
/// <summary>
/// Extension method for ISnapshotService which determines whether the given path is a symlink.
/// </summary>
/// <param name="snapshot">ISnapshotService implementation</param>
/// <param name="path">File or folder path</param>
/// <returns>Whether the path is a symlink</returns>
public static bool IsSymlink(this ISnapshotService snapshot, string path)
{
return snapshot.IsSymlink(path, snapshot.GetAttributes(path));
}
/// <summary>
/// Extension method for ISnapshotService which determines whether the given path is a symlink.
/// </summary>
/// <param name="snapshot">ISnapshotService implementation</param>
/// <param name="path">File or folder path</param>
/// <param name="attributes">File attributes</param>
/// <returns>Whether the path is a symlink</returns>
public static bool IsSymlink(this ISnapshotService snapshot, string path, FileAttributes attributes)
{
// Not all reparse points are symlinks.
// For example, on Windows 10 Fall Creator's Update, the OneDrive folder (and all subfolders)
// are reparse points, which allows the folder to hook into the OneDrive service and download things on-demand.
// If we can't find a symlink target for the current path, we won't treat it as a symlink.
return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && !string.IsNullOrEmpty(snapshot.GetSymlinkTarget(path));
}
/// <summary>
/// Extension method for ISystemIO which determines whether the given path is a symlink.
/// </summary>
/// <param name="systemIO">ISystemIO implementation</param>
/// <param name="path">File or folder path</param>
/// <returns>Whether the path is a symlink</returns>
public static bool IsSymlink(this ISystemIO systemIO, string path)
{
return systemIO.IsSymlink(path, systemIO.GetFileAttributes(path));
}
/// <summary>
/// Extension method for ISystemIO which determines whether the given path is a symlink.
/// </summary>
/// <param name="systemIO">ISystemIO implementation</param>
/// <param name="path">File or folder path</param>
/// <param name="attributes">File attributes</param>
/// <returns>Whether the path is a symlink</returns>
public static bool IsSymlink(this ISystemIO systemIO, string path, FileAttributes attributes)
{
// Not all reparse points are symlinks.
// For example, on Windows 10 Fall Creator's Update, the OneDrive folder (and all subfolders)
// are reparse points, which allows the folder to hook into the OneDrive service and download things on-demand.
// If we can't find a symlink target for the current path, we won't treat it as a symlink.
return (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && !string.IsNullOrEmpty(systemIO.GetSymlinkTarget(path));
}
/// <summary>
/// Gets an interface for System.IO, which wraps all operations in a platform consistent manner.
/// </summary>
@@ -104,6 +104,11 @@ namespace Duplicati.Library.Snapshots
{
UnixSupport.File.CreateSymlink(symlinkfile, target);
}
public string GetSymlinkTarget(string path)
{
return UnixSupport.File.GetSymlinkTarget(NoSnapshot.NormalizePath(path));
}
public string PathGetDirectoryName(string path)
{
@@ -21,6 +21,8 @@ using System.Collections.Generic;
using System.Security.AccessControl;
using System.IO;
using AlphaFS = Alphaleonis.Win32.Filesystem;
namespace Duplicati.Library.Snapshots
{
@@ -261,6 +263,32 @@ namespace Duplicati.Library.Snapshots
throw new System.IO.IOException(string.Format("Unable to create symlink, check account permissions: {0}", symlinkfile));
}
/// <summary>
/// Returns the symlink target if the entry is a symlink, and null otherwise
/// </summary>
/// <param name="file">The file or folder to examine</param>
/// <returns>The symlink target</returns>
public string GetSymlinkTarget(string file)
{
try
{
try
{
return AlphaFS.File.GetLinkTargetInfo(file).PrintName;
}
catch (PathTooLongException) { }
return AlphaFS.File.GetLinkTargetInfo(SystemIOWindows.PrefixWithUNC(file)).PrintName;
}
catch (AlphaFS.NotAReparsePointException) { }
catch (AlphaFS.UnrecognizedReparsePointException) { }
// This path looks like it isn't actually a symlink
// (Note that some reparse points aren't actually symlinks -
// things like the OneDrive folder in the Windows 10 Fall Creator's Update for example)
return null;
}
public IEnumerable<string> EnumerateFileSystemEntries(string path)
{
if (!IsPathTooLong(path))
+13 -17
View File
@@ -25,6 +25,8 @@ using System.Collections.Generic;
using System.IO;
using Alphaleonis.Win32.Vss;
using AlphaFS = Alphaleonis.Win32.Filesystem;
namespace Duplicati.Library.Snapshots
{
/// <summary>
@@ -129,7 +131,7 @@ namespace Duplicati.Library.Snapshots
m_volumes = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
foreach (string s in m_sourcepaths)
{
string drive = Alphaleonis.Win32.Filesystem.Path.GetPathRoot(s);
string drive = AlphaFS.Path.GetPathRoot(s);
if (!m_volumes.ContainsKey(drive))
{
if (!m_backup.IsVolumeSupported(drive))
@@ -198,14 +200,14 @@ namespace Duplicati.Library.Snapshots
/// <returns>A list of non-shadow paths</returns>
private string[] ListFolders(string folder)
{
string root = Utility.Utility.AppendDirSeparator(Alphaleonis.Win32.Filesystem.Path.GetPathRoot(folder));
string root = Utility.Utility.AppendDirSeparator(AlphaFS.Path.GetPathRoot(folder));
string volumePath = Utility.Utility.AppendDirSeparator(GetSnapshotPath(root));
string[] tmp = null;
string spath = GetSnapshotPath(folder);
if (SystemIOWindows.IsPathTooLong(spath))
try { tmp = Alphaleonis.Win32.Filesystem.Directory.GetDirectories(spath); }
try { tmp = AlphaFS.Directory.GetDirectories(spath); }
catch (PathTooLongException) { }
catch (DirectoryNotFoundException) { }
else
@@ -216,7 +218,7 @@ namespace Duplicati.Library.Snapshots
{
spath = SystemIOWindows.PrefixWithUNC(spath);
volumePath = SystemIOWindows.PrefixWithUNC(volumePath);
tmp = Alphaleonis.Win32.Filesystem.Directory.GetDirectories(spath);
tmp = AlphaFS.Directory.GetDirectories(spath);
}
volumePath = SystemIOWindows.PrefixWithUNC(volumePath);
@@ -235,14 +237,14 @@ namespace Duplicati.Library.Snapshots
/// <returns>A list of non-shadow paths</returns>
private string[] ListFiles(string folder)
{
string root = Utility.Utility.AppendDirSeparator(Alphaleonis.Win32.Filesystem.Path.GetPathRoot(folder));
string root = Utility.Utility.AppendDirSeparator(AlphaFS.Path.GetPathRoot(folder));
string volumePath = Utility.Utility.AppendDirSeparator(GetSnapshotPath(root));
string[] tmp = null;
string spath = GetSnapshotPath(folder);
if (SystemIOWindows.IsPathTooLong(spath))
try { tmp = Alphaleonis.Win32.Filesystem.Directory.GetFiles(spath); }
try { tmp = AlphaFS.Directory.GetFiles(spath); }
catch (PathTooLongException) { }
catch (DirectoryNotFoundException) { }
else
@@ -253,7 +255,7 @@ namespace Duplicati.Library.Snapshots
{
spath = SystemIOWindows.PrefixWithUNC(spath);
volumePath = SystemIOWindows.PrefixWithUNC(volumePath);
tmp = Alphaleonis.Win32.Filesystem.Directory.GetFiles(spath);
tmp = AlphaFS.Directory.GetFiles(spath);
}
volumePath = SystemIOWindows.PrefixWithUNC(volumePath);
@@ -273,7 +275,7 @@ namespace Duplicati.Library.Snapshots
if (!Path.IsPathRooted(localPath))
throw new InvalidOperationException();
string root = Alphaleonis.Win32.Filesystem.Path.GetPathRoot(localPath);
string root = AlphaFS.Path.GetPathRoot(localPath);
string volumePath;
if (!m_volumeMap.TryGetValue(root, out volumePath))
@@ -318,7 +320,7 @@ namespace Duplicati.Library.Snapshots
}
catch (PathTooLongException) { }
return Alphaleonis.Win32.Filesystem.File.GetLastWriteTimeUtc(SystemIOWindows.PrefixWithUNC(spath));
return AlphaFS.File.GetLastWriteTimeUtc(SystemIOWindows.PrefixWithUNC(spath));
}
/// <summary>
@@ -336,7 +338,7 @@ namespace Duplicati.Library.Snapshots
}
catch (PathTooLongException) { }
return Alphaleonis.Win32.Filesystem.File.GetCreationTimeUtc(SystemIOWindows.PrefixWithUNC(spath));
return AlphaFS.File.GetCreationTimeUtc(SystemIOWindows.PrefixWithUNC(spath));
}
/// <summary>
@@ -377,13 +379,7 @@ namespace Duplicati.Library.Snapshots
public string GetSymlinkTarget(string file)
{
string spath = GetSnapshotPath(file);
try
{
return Alphaleonis.Win32.Filesystem.File.GetLinkTargetInfo(spath).PrintName;
}
catch (PathTooLongException) { }
return Alphaleonis.Win32.Filesystem.File.GetLinkTargetInfo(SystemIOWindows.PrefixWithUNC(spath)).PrintName;
return _ioWin.GetSymlinkTarget(spath);
}
/// <summary>
@@ -37,6 +37,16 @@ namespace Duplicati.Library.UsageReporter
/// </summary>
private const int MAX_QUEUE_SIZE = 500;
/// <summary>
/// The maximum number of backlog files to consider
/// </summary>
private const int MAX_BACKLOG = 6;
/// <summary>
/// The maximum time a backlog file is considered valid
/// </summary>
private static readonly TimeSpan MAX_BACKLOG_AGE = TimeSpan.FromDays(30);
/// <summary>
/// The time to wait before sending event
/// </summary>
@@ -85,14 +95,7 @@ namespace Duplicati.Library.UsageReporter
return;
}
foreach (var f in GetAbandonedFiles(null))
{
// Check if we should exit
if (await self.Input.IsRetiredAsync)
return;
await self.Output.WriteAsync(f);
}
await ProcessAbandonedFiles(self.Output, self.Input, null);
var rs = new ReportSet();
var tf = GetTempFilename(instanceid);
@@ -131,13 +134,7 @@ namespace Duplicati.Library.UsageReporter
self.Output.WriteNoWait(tf);
rs = new ReportSet();
foreach (var f in GetAbandonedFiles(tf))
{
if (await self.Input.IsRetiredAsync)
return;
self.Output.WriteNoWait(f);
}
await ProcessAbandonedFiles(self.Output, self.Input, null);
tf = nextFilename;
}
@@ -148,18 +145,48 @@ namespace Duplicati.Library.UsageReporter
return new Tuple<Task, IWriteChannel<ReportItem>>(task, channel);
}
/// <summary>
/// Transmits abandoned files to the server, and enforces discard rules
/// </summary>
/// <returns>An awaitable task.</returns>
/// <param name="target">The target channel.</param>
/// <param name="source">The source channel, used to check for stopping conditions</param>
/// <param name="current">The current file, which should not be uploaded.</param>
private static async Task ProcessAbandonedFiles(IWriteChannel<string> target, IReadChannel<ReportItem> source, string current)
{
var abandonLeft = MAX_BACKLOG;
var abandonCutOff = long.Parse(DateTime.UtcNow.Add(-MAX_BACKLOG_AGE).ToString("yyyyMMddHHmmss"));
foreach (var f in GetAbandonedFiles(current))
{
if (await source.IsRetiredAsync)
return;
if (abandonLeft > 0 && f.Value > abandonCutOff)
{
abandonLeft--;
target.WriteNoWait(f.Key);
}
else
{
try { File.Delete(f.Key); }
catch { }
}
}
}
/// <summary>
/// Gets a list of abandoned files, meaning files that appear to be Duplicati files.
/// These should have been uploaded, but if they are found they are somehow left-over.
/// </summary>
/// <returns>The abandoned files.</returns>
/// <returns>The abandoned files, value is the timestamp, key is the filename.</returns>
/// <param name="current">The current file, which is excluded from the results.</param>
private static IEnumerable<string> GetAbandonedFiles(string current)
private static IEnumerable<KeyValuePair<string, long>> GetAbandonedFiles(string current)
{
return
from n in GetAbandonedMatches(current)
orderby n.Value
select n.Key;
return
GetAbandonedMatches(current)
.OrderByDescending(x => x.Value);
}
/// <summary>
+6 -1
View File
@@ -114,6 +114,10 @@ namespace Duplicati.Library.Utility
// If no filter sets are specified, we use the default for the platform we're on
if (!anyOptions)
{
// Until we agree on how default filters should be applied,
// we do not apply default filters
/*
if (Utility.IsClientWindows)
{
filterSets |= DefaultFilterSet.Windows;
@@ -127,7 +131,8 @@ namespace Duplicati.Library.Utility
if (Utility.IsClientLinux)
{
filterSets |= DefaultFilterSet.Linux;
}
}*/
}
if (filterSets == DefaultFilterSet.None)
+7 -7
View File
@@ -610,19 +610,19 @@ namespace Duplicati.Library.Utility
/// </summary>
/// <param name="size">The size to format</param>
/// <returns>A human readable string representing the size</returns>
public static string FormatSizeString(long size)
public static string FormatSizeString(double size)
{
long sizeAbs = Math.Abs(size); // Allow formatting of negative sizes
double sizeAbs = Math.Abs(size); // Allow formatting of negative sizes
if (sizeAbs >= 1024 * 1024 * 1024 * 1024L)
return Strings.Utility.FormatStringTB((double)size / (1024 * 1024 * 1024 * 1024L));
return Strings.Utility.FormatStringTB(size / (1024 * 1024 * 1024 * 1024L));
else if (sizeAbs >= 1024 * 1024 * 1024)
return Strings.Utility.FormatStringGB((double)size / (1024 * 1024 * 1024));
return Strings.Utility.FormatStringGB(size / (1024 * 1024 * 1024));
else if (sizeAbs >= 1024 * 1024)
return Strings.Utility.FormatStringMB((double)size / (1024 * 1024));
return Strings.Utility.FormatStringMB(size / (1024 * 1024));
else if (sizeAbs >= 1024)
return Strings.Utility.FormatStringKB((double)size / 1024);
return Strings.Utility.FormatStringKB(size / 1024);
else
return Strings.Utility.FormatStringB(size);
return Strings.Utility.FormatStringB((long) size); // safe to cast because lower than 1024 and thus well within range of long
}
public static System.Threading.ThreadPriority ParsePriority(string value)
+34
View File
@@ -162,6 +162,40 @@ namespace Duplicati.Library.Utility
WorkQueueChanged(this);
}
/// <summary>
/// An overloaded AddTask method that allows a task to skip to the front of a queue
/// It does this by creating a new queue, adding the new task first, and then adding
/// all the old tasks to the new queue. It's cleaner to use a linked list,
/// but the performance difference is negligible on such a small queue.
/// </summary>
/// <param name="task">Task.</param>
/// <param name="skipQueue">If set to <c>true</c> skip queue.</param>
public void AddTask(Tx task, bool skipQueue)
{
if (!skipQueue) {
// Fall back to default AddTask method
AddTask(task);
return;
}
lock (m_lock)
{
Queue<Tx> newQueue = new Queue<Tx>();
newQueue.Enqueue(task);
while (m_tasks.Count > 0)
{
Tx n = m_tasks.Dequeue();
newQueue.Enqueue(n);
}
m_tasks = newQueue;
m_event.Set();
}
if (WorkQueueChanged != null)
WorkQueueChanged(this);
}
/// <summary>
/// Removes a task from the queue, does not remove the task if it is currently running
/// </summary>
+10 -2
View File
@@ -235,7 +235,7 @@ namespace Duplicati.Server
}
}
public LogEntry[] AfterID(long id, LogMessageType level)
public LogEntry[] AfterID(long id, LogMessageType level, int pagesize)
{
RenewTimeout(level);
UpdateLogLevel();
@@ -245,7 +245,15 @@ namespace Duplicati.Server
if (m_buffer == null)
return new LogEntry[0];
return m_buffer.FlatArray((x) => x.ID > id && x.Type >= level );
var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level );
// Return the <page_size> newest entries
if (buffer.Length > pagesize) {
var index = buffer.Length - pagesize;
return buffer.Skip(index).Take(pagesize).ToArray();
}
else {
return buffer;
}
}
}
+25 -10
View File
@@ -512,7 +512,7 @@ namespace Duplicati.Server
}
}
using(tempfolder)
using (tempfolder)
using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink))
{
try
@@ -536,7 +536,14 @@ namespace Duplicati.Server
{
case DuplicatiOperation.Backup:
{
var filter = ApplyFilter(backup, data.Operation, GetCommonFilter(backup, data.Operation));
IEnumerable<Library.Utility.IFilter> defaultFilters = null;
string defaultFiltersValue;
if (options.TryGetValue("default-filters", out defaultFiltersValue) || options.TryGetValue("default-filter", out defaultFiltersValue))
{
defaultFilters = Library.Utility.DefaultFilters.GetFilters(Library.Utility.Utility.ExpandEnvironmentVariables(defaultFiltersValue ?? string.Empty));
}
var filter = ApplyFilter(backup, data.Operation, GetCommonFilter(backup, data.Operation), defaultFilters);
var sources =
(from n in backup.Sources
let p = SpecialFolders.ExpandEnvironmentVariables(n)
@@ -747,7 +754,10 @@ namespace Duplicati.Server
"Warning" : string.Format("Warning while running {0}", backup.Name),
r.FilesWithError > 0 ?
string.Format("Errors affected {0} file(s) ", r.FilesWithError) :
string.Format("Got {0} warning(s) ", r.Warnings.Count())
(r.Errors.Any() ?
string.Format("Got {0} error(s)", r.Errors.Count()) :
string.Format("Got {0} warning(s)", r.Warnings.Count())
)
,
null,
backup.ID,
@@ -848,7 +858,7 @@ namespace Duplicati.Server
return options;
}
private static Duplicati.Library.Utility.IFilter ApplyFilter(Duplicati.Server.Serialization.Interface.IBackup backup, DuplicatiOperation mode, Duplicati.Library.Utility.IFilter filter)
private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, DuplicatiOperation mode, Library.Utility.IFilter filter, IEnumerable<Library.Utility.IFilter> defaultFilters)
{
var f2 = backup.Filters;
if (f2 != null && f2.Length > 0)
@@ -860,15 +870,20 @@ namespace Duplicati.Server
? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression)
: SpecialFolders.ExpandEnvironmentVariables(n.Expression)
orderby n.Order
select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
.Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b));
select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include)))
.Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b));
return Duplicati.Library.Utility.FilterExpression.Combine(filter, nf);
filter = Library.Utility.FilterExpression.Combine(filter, nf);
}
else
return filter;
if (defaultFilters != null && defaultFilters.Any())
{
filter = Library.Utility.FilterExpression.Combine(filter, defaultFilters.Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b)));
}
return filter;
}
private static Dictionary<string, string> GetCommonOptions(Duplicati.Server.Serialization.Interface.IBackup backup, DuplicatiOperation mode)
{
return
@@ -51,9 +51,6 @@ namespace Duplicati.Server.WebServer
{
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
if (string.IsNullOrWhiteSpace(xsrftoken))
xsrftoken = Duplicati.Library.Utility.Uri.UrlDecode(xsrftoken);
if (string.IsNullOrWhiteSpace(xsrftoken))
{
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
@@ -289,7 +286,7 @@ namespace Duplicati.Server.WebServer
else
{
response.Status = System.Net.HttpStatusCode.BadRequest;
response.Reason = "Missing XSRF Token";
response.Reason = "Missing XSRF Token. Please reload the page";
return true;
}
+3 -1
View File
@@ -38,10 +38,12 @@ namespace Duplicati.Server.WebServer
}
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
: base(resp.Body, resp.Encoding)
: base(resp.Body, resp.Encoding)
{
m_resp = resp;
m_jsonp = jsonp;
if (!m_resp.HeadersSent)
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
}
protected override void Dispose (bool disposing)
@@ -49,6 +49,7 @@ namespace Duplicati.Server.WebServer
response.Status = System.Net.HttpStatusCode.OK;
response.Reason = "OK";
response.ContentType = "text/html; charset=utf-8";
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm))
{
@@ -47,7 +47,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (string.IsNullOrWhiteSpace(timestring) && !allversion)
{
info.ReportClientError("Invalid or missing time");
info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -293,7 +293,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
else
{
Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup));
Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true);
Program.StatusEventNotifyer.SignalNewEvent();
}
@@ -329,13 +329,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
var np = info.Request.Form["path"].Value;
if (string.IsNullOrWhiteSpace(np))
info.ReportClientError("No target path supplied");
info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest);
else if (!Path.IsPathRooted(np))
info.ReportClientError("Target path is relative, please supply a fully qualified path");
info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest);
else
{
if (move && (File.Exists(np) || Directory.Exists(np)))
info.ReportClientError("A file already exists at the new location");
info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict);
else
{
if (move)
@@ -387,7 +387,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
IsActive(bk, info);
return;
default:
info.ReportClientError(string.Format("Invalid component: {0}", operation));
info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -416,7 +416,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var parts = (key ?? "").Split(new char[] { '/' }, 2);
var bk = Program.DataConnection.GetBackup(parts.First());
if (bk == null)
info.ReportClientError("Invalid or missing backup id");
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
else
{
if (parts.Length > 1)
@@ -483,7 +483,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
}
info.ReportClientError("Invalid request");
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
}
}
@@ -495,7 +495,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (string.IsNullOrWhiteSpace(str))
{
info.ReportClientError("Missing backup object");
info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -505,7 +505,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
data = Serializer.Deserialize<Backups.AddOrUpdateBackupData>(new StringReader(str));
if (data.Backup == null)
{
info.ReportClientError("Data object had no backup entry");
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -514,7 +514,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (string.IsNullOrEmpty(data.Backup.ID))
{
info.ReportClientError("Invalid or missing backup id");
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -535,20 +535,20 @@ namespace Duplicati.Server.WebServer.RESTMethods
var backup = Program.DataConnection.GetBackup(data.Backup.ID);
if (backup == null)
{
info.ReportClientError("Invalid or missing backup id");
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
return;
}
if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID).Any())
{
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name);
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
return;
}
var err = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err);
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -563,9 +563,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
@@ -574,7 +574,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var backup = Program.DataConnection.GetBackup(key);
if (backup == null)
{
info.ReportClientError("Invalid or missing backup id");
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
return;
}
@@ -586,7 +586,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var captcha_answer = info.Request.Param["captcha-answer"].Value;
if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer))
{
info.ReportClientError("Missing captcha");
info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized);
return;
}
@@ -120,7 +120,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var err = Program.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err);
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -168,7 +168,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
data = Serializer.Deserialize<AddOrUpdateBackupData>(new StringReader(str));
if (data.Backup == null)
{
info.ReportClientError("Data object had no backup entry");
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -200,14 +200,14 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (Program.DataConnection.Backups.Where(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase)).Any())
{
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name);
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
return;
}
var err = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err);
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -220,9 +220,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
@@ -29,13 +29,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
var tf = Program.DataConnection.GetTempFiles().Where(x => x.ID == id).FirstOrDefault();
if (tf == null)
{
info.ReportClientError("Invalid or missing bugreport id");
info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound);
return;
}
if (!System.IO.File.Exists(tf.Path))
{
info.ReportClientError("File is missing");
info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound);
return;
}
@@ -61,7 +61,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("Missing token value");
info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized);
return;
}
else
@@ -105,7 +105,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var target = info.Request.Param["target"].Value;
if (string.IsNullOrWhiteSpace(target))
{
info.ReportClientError("Missing target parameter");
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -147,19 +147,19 @@ namespace Duplicati.Server.WebServer.RESTMethods
var target = info.Request.Param["target"].Value;
if (string.IsNullOrWhiteSpace(answer))
{
info.ReportClientError("Missing answer parameter");
info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
if (string.IsNullOrWhiteSpace(target))
{
info.ReportClientError("Missing target parameter");
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
if (SolvedCaptcha(key, target, answer))
info.OutputOK();
else
info.ReportClientError("Incorrect");
info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden);
}
}
}
@@ -45,7 +45,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var updateInfo = Program.DataConnection.ApplicationSettings.UpdatedVersion;
if (updateInfo == null)
{
info.ReportClientError("No update found");
info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound);
}
else
{
@@ -148,14 +148,14 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase))
{
info.ReportClientError("Only abort commands are allowed");
info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest);
return;
}
key = key.Substring(0, key.Length - "/abort".Length);
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("No task key found");
info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -18,6 +18,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Duplicati.Library.Snapshots;
namespace Duplicati.Server.WebServer.RESTMethods
{
@@ -38,7 +39,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (string.IsNullOrEmpty(path))
{
info.ReportClientError("No path parameter was found");
info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -67,7 +68,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (Duplicati.Library.Utility.Utility.IsClientLinux && !path.StartsWith("/", StringComparison.Ordinal))
{
info.ReportClientError("The path parameter must start with a forward-slash");
info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -87,12 +88,12 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
}
info.ReportServerError("File or folder not found");
info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound);
return;
}
else
{
info.ReportClientError(string.Format("No such operation found: {0}", command));
info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound);
return;
}
}
@@ -143,7 +144,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
catch (Exception ex)
{
info.ReportClientError("Failed to process the path: " + ex.Message);
info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError);
}
}
@@ -215,7 +216,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
try
{
var attr = systemIO.GetFileAttributes(s);
var isSymlink = (attr & FileAttributes.ReparsePoint) != 0;
var isSymlink = systemIO.IsSymlink(s, attr);
var isFolder = (attr & FileAttributes.Directory) != 0;
var isFile = !isFolder;
var isHidden = (attr & FileAttributes.Hidden) != 0;
@@ -62,7 +62,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (foundVMs.Count == 1)
info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
else
info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key));
info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
}
}
catch (Exception ex)
@@ -29,13 +29,19 @@ namespace Duplicati.Server.WebServer.RESTMethods
var level_str = input["level"].Value ?? "";
var id_str = input["id"].Value ?? "";
int pagesize;
if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize))
pagesize = 100;
pagesize = Math.Max(1, Math.Min(500, pagesize));
Library.Logging.LogMessageType level;
long id;
long.TryParse(id_str, out id);
Enum.TryParse(level_str, true, out level);
info.OutputOK(Program.LogHandler.AfterID(id, level));
info.OutputOK(Program.LogHandler.AfterID(id, level, pagesize));
}
else
{
@@ -60,7 +60,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (foundDBs.Count == 1)
info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
else
info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key));
info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
}
}
catch (Exception ex)
@@ -20,6 +20,6 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public class Notification : IRESTMethodGET, IRESTMethodDELETE
{
public void GET(string key, RequestInfo info) { long id; if (!long.TryParse(key, out id)) { info.ReportClientError("Invalid ID"); return; } var el = Program.DataConnection.GetNotifications().Where(x => x.ID == id).FirstOrDefault(); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else info.OutputOK(el); } public void DELETE(string key, RequestInfo info) { long id; if (!long.TryParse(key, out id)) { info.ReportClientError("Invalid ID"); return; } var el = Program.DataConnection.GetNotifications().Where(x => x.ID == id).FirstOrDefault(); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else { Program.DataConnection.DismissNotification(id); info.OutputOK(); } } }
public void GET(string key, RequestInfo info) { long id; if (!long.TryParse(key, out id)) { info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); return; } var el = Program.DataConnection.GetNotifications().Where(x => x.ID == id).FirstOrDefault(); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else info.OutputOK(el); } public void DELETE(string key, RequestInfo info) { long id; if (!long.TryParse(key, out id)) { info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest); return; } var el = Program.DataConnection.GetNotifications().Where(x => x.ID == id).FirstOrDefault(); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else { Program.DataConnection.DismissNotification(id); info.OutputOK(); } } }
}
@@ -20,6 +20,6 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public class ProgressState : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info) { if (Program.GenerateProgressState == null) info.ReportClientError("No active backup"); else info.OutputOK(Program.GenerateProgressState()); } public string Description { get { return "Return the progress of the currently running operation."; } } public IEnumerable<KeyValuePair<string, Type>> Types { get { return new KeyValuePair<string, Type>[] { new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData)) }; } } }
public void GET(string key, RequestInfo info) { if (Program.GenerateProgressState == null) info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound); else info.OutputOK(Program.GenerateProgressState()); } public string Description { get { return "Return the progress of the currently running operation."; } } public IEnumerable<KeyValuePair<string, Type>> Types { get { return new KeyValuePair<string, Type>[] { new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData)) }; } } }
}
@@ -125,7 +125,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (parts.Length <= 1)
{
info.ReportClientError("No url or operation supplied");
info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -147,7 +147,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
TestConnection(url, info);
return;
default:
info.ReportClientError("No such method");
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
return;
}
}
@@ -177,7 +177,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
TestConnection(url, info);
return;
default:
info.ReportClientError("No such method");
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
return;
}
}
@@ -22,6 +22,6 @@ namespace Duplicati.Server.WebServer.RESTMethods
{ public HttpServer.IHttpRequest Request { get; private set; } public HttpServer.IHttpResponse Response { get; private set; } public HttpServer.Sessions.IHttpSession Session { get; private set; } public BodyWriter BodyWriter { get; private set; }
public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{ Request = request; Response = response; Session = session; BodyWriter = new BodyWriter(response, request);
} public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError) { Response.Status = code; Response.Reason = message; BodyWriter.WriteJsonObject(new { Error = message }); } public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest) { ReportServerError(message, code); } public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError) { HttpServer.HttpInput input = Request.Method.ToUpper() == "POST" ? Request.Form : Request.QueryString; if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) { long lastEventId; if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) { ReportClientError("When activating long poll, the request must include the last event id"); isError = true; return false; } TimeSpan ts; try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } catch (Exception ex) { ReportClientError("Invalid duration: " + ex.Message); isError = true; return false; } if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) { ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds"); isError = true; return false; } isError = false; id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); return true; } isError = false; return false; } public void OutputOK(object item = null) { BodyWriter.OutputOK(item); } public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null) { Response.Status = code; Response.Reason = reason ?? "Error"; BodyWriter.WriteJsonObject(item); } public void Dispose() { if (BodyWriter != null) { var bw = BodyWriter; BodyWriter = null; bw.Dispose(); } } }
} public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError) { Response.Status = code; Response.Reason = message; BodyWriter.WriteJsonObject(new { Error = message }); } public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest) { ReportServerError(message, code); } public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError) { HttpServer.HttpInput input = Request.Method.ToUpper() == "POST" ? Request.Form : Request.QueryString; if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false)) { long lastEventId; if (!long.TryParse(input["lasteventid"].Value, out lastEventId)) { ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest); isError = true; return false; } TimeSpan ts; try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); } catch (Exception ex) { ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest); isError = true; return false; } if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue) { ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest); isError = true; return false; } isError = false; id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds); return true; } isError = false; return false; } public void OutputOK(object item = null) { BodyWriter.OutputOK(item); } public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null) { Response.Status = code; Response.Reason = reason ?? "Error"; BodyWriter.WriteJsonObject(item); } public void Dispose() { if (BodyWriter != null) { var bw = BodyWriter; BodyWriter = null; bw.Dispose(); } } }
}
@@ -56,7 +56,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (string.IsNullOrWhiteSpace(str))
{
info.ReportClientError("Missing data object");
info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -66,7 +66,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
data = Serializer.Deserialize<Dictionary<string, string>>(new StringReader(str));
if (data == null)
{
info.ReportClientError("Data object had no entry");
info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -104,9 +104,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
@@ -55,7 +55,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
catch (Exception ex)
{
info.ReportClientError(ex.Message);
info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest);
return;
}
if (ts.TotalMilliseconds > 0)
@@ -60,7 +60,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
else
{
info.ReportClientError("Invalid request");
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
}
}
@@ -97,7 +97,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
}
info.ReportClientError("Invalid or missing task id");
info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound);
}
}
}
@@ -30,7 +30,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("Scheme is missing");
info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest);
return;
}
@@ -41,13 +41,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
catch (Exception ex)
{
info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message));
info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
return;
}
if (data == null)
{
info.ReportClientError(string.Format("Unable to parse settings object"));
info.ReportClientError(string.Format("Unable to parse settings object"), System.Net.HttpStatusCode.BadRequest);
return;
}
+25 -4
View File
@@ -271,26 +271,26 @@ namespace Duplicati.Server.WebServer
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
{
var customized_files = new FileModule("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
AddMimeTypes(customized_files);
server.Add(customized_files);
}
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
{
var oem_files = new FileModule("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
AddMimeTypes(oem_files);
server.Add(oem_files);
}
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
{
var proxy_files = new FileModule("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
AddMimeTypes(proxy_files);
server.Add(proxy_files);
}
var fh = new FileModule("/", webroot, true);
var fh = new CacheControlFileHandler("/", webroot, true);
AddMimeTypes(fh);
server.Add(fh);
@@ -310,5 +310,26 @@ namespace Duplicati.Server.WebServer
return false;
}
}
private class CacheControlFileHandler : FileModule
{
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
: base(baseUri, basePath, useLastModifiedHeader)
{
}
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
if (!this.CanHandle(request.Uri))
return false;
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
else
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
return base.Process(request, response, session);
}
}
}
}
@@ -204,19 +204,14 @@ ul.tabs {
}
.entries {
div.entryline {
div.entryline.clickable {
cursor: pointer;
}
}
.entries.livedata {
li {
height: 1.2em;
}
li.expanded {
height: auto;
overflow: auto;
}
}
@@ -677,6 +672,7 @@ body {
width: 260px;
padding-left: 40px;
float: left;
position: fixed;
> ul {
> li {
@@ -883,7 +879,7 @@ body {
.content {
float: left;
padding-left: 50px;
padding-left: 350px;
padding-bottom: 50px;
max-width: 700px;
@@ -1491,7 +1487,7 @@ body {
ul.entries {
li {
padding-top: 30px;
padding-top: 10px;
}
}
}
File diff suppressed because one or more lines are too long
@@ -315,21 +315,26 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout
return;
}
if ($scope.KeepType == 'time' || $scope.KeepType == '')
{
delete opts['keep-versions'];
delete opts['retention-policy'];
// Retention options are mutual exclusive -> allow only one to be selected at a time
function resetAllRetentionOptionsExcept(optionToKeep = '') {
['keep-versions', 'keep-time', 'retention-policy'].forEach(function(entry) {
if (entry != optionToKeep) {
delete opts[entry];
}
});
}
if ($scope.KeepType == 'versions' || $scope.KeepType == '')
{
delete opts['keep-time'];
delete opts['retention-policy'];
}
if ($scope.KeepType == 'smart' || $scope.KeepType == '')
{
delete opts['keep-versions'];
delete opts['keep-time'];
delete opts['retention-policy'];
if ($scope.KeepType == 'time') {
resetAllRetentionOptionsExcept('keep-time');
} else if ($scope.KeepType == 'versions') {
resetAllRetentionOptionsExcept('keep-versions');
} else if ($scope.KeepType == 'smart' || $scope.KeepType == 'custom') {
resetAllRetentionOptionsExcept('retention-policy');
} else {
resetAllRetentionOptionsExcept(); // keep none
}
if ($scope.KeepType == 'time' && (opts['keep-time'] || '').trim().length == 0)
@@ -32,7 +32,8 @@ backupApp.controller('LogController', function($scope, $routeParams, $timeout, S
resp.data.reverse();
$scope.LiveData.unshift.apply($scope.LiveData, resp.data);
$scope.LiveData.Length = Math.min(1000, $scope.LiveData.length);
$scope.LiveData.Length = Math.min(300, $scope.LiveData.length);
$scope.LiveData = $scope.LiveData.slice(0,$scope.LiveData.Length)
$scope.LiveRefreshing = false;
if ($scope.LiveRefreshPending)
File diff suppressed because one or more lines are too long
@@ -335,7 +335,7 @@
<input type="text" ng-model="Options['retention-policy']" ng-show="KeepType == 'custom'" />
</div>
<div class="hint" translate ng-show="KeepType == 'custom'">
Enter a retention strategy manually. Placeholders are D/W/Y for days/weeks/years. The syntax is: 7D:1D,4W:1W,36M:1M. This example keeps one backup for each of the next 7 days, one for each of the next 4 weeks, and one for each of the next 36 months. This can also be written as 1W:1D,1M:1W,3Y:1M.
Enter a retention strategy manually. Placeholders are D/W/Y for days/weeks/years and U for unlimited. The syntax is: 7D:1D,4W:1W,36M:1M. This example keeps one backup for each of the next 7 days, one for each of the next 4 weeks, and one for each of the next 36 months. This can also be written as 1W:1D,1M:1W,3Y:1M.
</div>
<div class="retention-options" ng-show="KeepType == 'versions'">
@@ -34,7 +34,7 @@
<ul class="entries livedata">
<li ng-repeat="item in LiveData" ng-class="{expanded: expanded}">
<div ng-click="expanded = !expanded" class="entryline" ng-class="{noexception: item.Exception == null}">{{item.When | parsetimestamp}}: {{item.Message}}</div>
<div ng-click="expanded = !expanded" class="entryline" ng-class="{noexception: item.Exception == null, clickable: item.Exception != null}">{{item.When | parsetimestamp}}: {{item.Message}}</div>
<div ng-show="expanded &amp;&amp; item.Exception != null" class="prewrapped-text exceptiontext">{{item.Exception}}</div>
</li>
</ul>
@@ -50,6 +50,10 @@
<Project>{7E119745-1F62-43F0-936C-F312A1912C0B}</Project>
<Name>Duplicati.Library.AutoUpdater</Name>
</ProjectReference>
<ProjectReference Include="..\Library\Utility\Duplicati.Library.Utility.csproj">
<Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
<Name>Duplicati.Library.Utility</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
+3 -2
View File
@@ -16,6 +16,7 @@
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Linq;
namespace Duplicati.Service
{
@@ -57,7 +58,7 @@ namespace Duplicati.Service
var exec = System.IO.Path.Combine(path, "Duplicati.Server.exe");
var cmdargs = "--ping-pong-keepalive=true";
if (m_cmdargs != null && m_cmdargs.Length > 0)
cmdargs = cmdargs + " " + string.Join(" ", m_cmdargs);
cmdargs = Duplicati.Library.Utility.Utility.WrapAsCommandLine(new string[] { cmdargs }.Concat(m_cmdargs));
var firstRun = true;
var startAttempts = 0;
@@ -92,8 +93,8 @@ namespace Duplicati.Service
{
PingProcess();
m_onStartedAction();
firstRun = false;
}
firstRun = false;
while (!m_process.HasExited)
{
+10
View File
@@ -32,6 +32,16 @@ namespace Duplicati.UnitTest
Directory.CreateDirectory(TARGETFOLDER);
}
[Test]
[Category("Border")]
public void Run10kNoProgress()
{
PrepareSourceData();
RunCommands(1024 * 10, modifyOptions: opts => {
opts["disable-file-scanner"] = "true";
});
}
[Test]
[Category("Border")]
public void Run10k()
+8 -1
View File
@@ -23,6 +23,13 @@ namespace Duplicati.UnitTest
public RandomErrorBackend()
{
} public RandomErrorBackend(string url, Dictionary<string, string> options) { var u = new Library.Utility.Uri(url).SetScheme(WrappedBackend).ToString(); m_backend = (IStreamingBackend)Library.DynamicLoader.BackendLoader.GetBackend(u, options); } private void ThrowErrorRandom() { if (random.NextDouble() > 0.90) throw new Exception("Random upload failure"); }
#region IStreamingBackend implementation public void Put(string remotename, Stream stream) { var uploadError = random.NextDouble() > 0.9; using(var f = new Library.Utility.ProgressReportingStream(stream, stream.Length, x => { if (uploadError && stream.Position > stream.Length / 2) throw new Exception("Random upload failure"); })) m_backend.Put(remotename, f); ThrowErrorRandom(); } public void Get(string remotename, Stream stream) { ThrowErrorRandom(); m_backend.Get(remotename, stream); ThrowErrorRandom(); } #endregion #region IBackend implementation public IEnumerable<IFileEntry> List() { return m_backend.List(); } public void Put(string remotename, string filename) { ThrowErrorRandom(); m_backend.Put(remotename, filename); ThrowErrorRandom(); } public void Get(string remotename, string filename) { ThrowErrorRandom(); m_backend.Get(remotename, filename); ThrowErrorRandom(); } public void Delete(string remotename) { ThrowErrorRandom(); m_backend.Delete(remotename); ThrowErrorRandom(); } public void Test() { m_backend.Test(); } public void CreateFolder() { m_backend.CreateFolder(); } public string DisplayName { get { return "Random Error Backend"; } } public string ProtocolKey { get { return "randomerror"; } } public IList<ICommandLineArgument> SupportedCommands { get { if (m_backend == null) try { return Duplicati.Library.DynamicLoader.BackendLoader.GetSupportedCommands(WrappedBackend + "://"); } catch { } return m_backend.SupportedCommands; } } public string Description { get { return "A testing backend that randomly fails"; } } #endregion #region IDisposable implementation public void Dispose() { if (m_backend != null) try { m_backend.Dispose(); } finally { m_backend = null; } } #endregion }
#region IStreamingBackend implementation public void Put(string remotename, Stream stream) { var uploadError = random.NextDouble() > 0.9; using(var f = new Library.Utility.ProgressReportingStream(stream, stream.Length, x => { if (uploadError && stream.Position > stream.Length / 2) throw new Exception("Random upload failure"); })) m_backend.Put(remotename, f); ThrowErrorRandom(); } public void Get(string remotename, Stream stream) { ThrowErrorRandom(); m_backend.Get(remotename, stream); ThrowErrorRandom(); } #endregion #region IBackend implementation public IEnumerable<IFileEntry> List() { return m_backend.List(); } public void Put(string remotename, string filename) { ThrowErrorRandom(); m_backend.Put(remotename, filename); ThrowErrorRandom(); } public void Get(string remotename, string filename) { ThrowErrorRandom(); m_backend.Get(remotename, filename); ThrowErrorRandom(); } public void Delete(string remotename) { ThrowErrorRandom(); m_backend.Delete(remotename); ThrowErrorRandom(); } public void Test() { m_backend.Test(); } public void CreateFolder() { m_backend.CreateFolder(); }
public string[] DNSName
{
get
{
return m_backend.DNSName;
}
} public string DisplayName { get { return "Random Error Backend"; } } public string ProtocolKey { get { return "randomerror"; } } public IList<ICommandLineArgument> SupportedCommands { get { if (m_backend == null) try { return Duplicati.Library.DynamicLoader.BackendLoader.GetSupportedCommands(WrappedBackend + "://"); } catch { } return m_backend.SupportedCommands; } } public string Description { get { return "A testing backend that randomly fails"; } } #endregion #region IDisposable implementation public void Dispose() { if (m_backend != null) try { m_backend.Dispose(); } finally { m_backend = null; } } #endregion }
}
@@ -78,6 +78,13 @@ namespace Duplicati.UnitTest
{
m_backend.CreateFolder();
}
public string[] DNSName
{
get
{
return m_backend.DNSName;
}
}
public string DisplayName
{
get
+1 -1
View File
@@ -47,7 +47,7 @@ namespace Duplicati.WindowsService
else if (install || uninstall)
{
// Remove the install and uninstall flags if they are present
var commandline = string.Join(" ", args.Where(x => !(string.Equals("install", x, StringComparison.OrdinalIgnoreCase) || string.Equals("uninstall", x, StringComparison.OrdinalIgnoreCase))));
var commandline = Library.Utility.Utility.WrapAsCommandLine(args.Where(x => !(string.Equals("install", x, StringComparison.OrdinalIgnoreCase) || string.Equals("uninstall", x, StringComparison.OrdinalIgnoreCase))));
var selfexec = Assembly.GetExecutingAssembly().Location;
@@ -79,6 +79,10 @@
<Project>{7E119745-1F62-43F0-936C-F312A1912C0B}</Project>
<Name>Duplicati.Library.AutoUpdater</Name>
</ProjectReference>
<ProjectReference Include="..\Library\Utility\Duplicati.Library.Utility.csproj">
<Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
<Name>Duplicati.Library.Utility</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+64
View File
@@ -0,0 +1,64 @@
# [Duplicati](https://www.duplicati.com)
Duplicati is a free, open source, backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers. It works with:
*Amazon S3, OneDrive, Google Drive, Rackspace Cloud Files, HubiC, Backblaze (B2), Amazon Cloud Drive (AmzCD), Swift / OpenStack, WebDAV, SSH (SFTP), FTP, and more!*
Duplicati is licensed under LGPL and available for Windows, OSX and Linux (.NET 4.5+ or Mono required).
## Available tags
* `beta` - the most recent beta release
* `experimental` - the most recent experimental release
* `canary` - the most recent canary release
* `latest` - an alias for `beta`
* specific versions like `2.0.2.1_beta_2017-08-01`
Images for the following OS/architecture combinations are available:
* `linux-amd64`
* `linux-arm32v7` - 32-bit ARMv7 devices like the Raspberry Pi 2
The default architecture is `linux-amd64`. To pull an image for another architecture, prepend the architecture string to the image tag, e.g. `linux-arm32v7-beta`.
## How to use this image
```console
$ docker run -p 8200:8200 -v /some/path:/some/path duplicati/duplicati
```
Then, open [http://localhost:8200](http://localhost:8200) on the host to access the Duplicati web interface and configure backups. Any host directory that you want to back up needs to be mounted into the container using the `-v` option.
### Preserving configuration
All configuration is stored in `/data` inside the container, so you can mount a volume at that path to preserve the configuration:
```console
$ docker run --name=duplicati -v duplicati-data:/data duplicati/duplicati
```
This allows you to delete and recreate the container without losing your configuration:
```console
$ docker rm duplicati
$ docker run --name=duplicati -v duplicati-data:/data duplicati/duplicati
```
### Using Duplicati CLI
Run the `duplicati-cli` command to use the Duplicati command-line interface:
```console
$ docker run --rm duplicati/duplicati duplicati-cli help
See duplicati.commandline.exe help <topic> for more information.
General: example, changelog
...
$ docker run --rm -v /home:/backup/home duplicati/duplicati duplicati-cli backup ssh://user@host /backup/home
```
### Specifying server arguments
To launch the Duplicati server with additional arguments, run the `duplicati-server` command:
```console
$ docker run duplicati/duplicati duplicati-server --log-level=debug
```
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
if [ ! -f "$1" ]; then
echo "Please provide the filename of an existing zip build as the first argument"
exit
fi
ARCHITECTURES="amd64 arm32v7"
DEFAULT_ARCHITECTURE=amd64
DEFAULT_CHANNEL=beta
REPOSITORY=duplicati/duplicati
ARCHIVE_NAME=$(basename -s .zip $1)
VERSION=$(echo "${ARCHIVE_NAME}" | cut -d "-" -f 2-)
CHANNEL=$(echo "${ARCHIVE_NAME}" | cut -d "_" -f 2)
DIRNAME=duplicati
if [ -d "${DIRNAME}" ]; then
rm -rf "${DIRNAME}"
fi
unzip -d "${DIRNAME}" "$1"
for n in "../oem" "../../oem" "../../../oem"
do
if [ -d $n ]; then
echo "Installing OEM files"
cp -R $n "${DIRNAME}/webroot/"
fi
done
for n in "oem-app-name.txt" "oem-update-url.txt" "oem-update-key.txt" "oem-update-readme.txt" "oem-update-installid.txt"
do
for p in "../$n" "../../$n" "../../../$n"
do
if [ -f $p ]; then
echo "Installing OEM override file"
cp $p "${DIRNAME}"
fi
done
done
for arch in ${ARCHITECTURES}; do
tags="linux-${arch}-${VERSION} linux-${arch}-${CHANNEL}"
if [ ${CHANNEL} = ${DEFAULT_CHANNEL} ]; then
tags="linux-${arch}-latest ${tags}"
fi
if [ ${arch} = ${DEFAULT_ARCHITECTURE} ]; then
tags="${VERSION} ${CHANNEL} ${tags}"
fi
if [ ${CHANNEL} = ${DEFAULT_CHANNEL} -a ${arch} = ${DEFAULT_ARCHITECTURE} ]; then
tags="latest ${tags}"
fi
args=""
for tag in ${tags}; do
args="-t ${REPOSITORY}:${tag} ${args}"
done
docker build \
${args} \
--build-arg ARCH=${arch}/ \
--build-arg VERSION=${VERSION} \
--build-arg CHANNEL=${CHANNEL} \
--file context/Dockerfile \
.
for tag in ${tags}; do
docker push ${REPOSITORY}:${tag}
done
done
rm -rf "${DIRNAME}"
+31
View File
@@ -0,0 +1,31 @@
ARG ARCH=
FROM ${ARCH}mono:5-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
libmono-sqlite4.0-cil \
libmono-system-drawing4.0-cil \
referenceassemblies-pcl && \
rm -rf /var/lib/apt/lists && \
cert-sync /etc/ssl/certs/ca-certificates.crt
ENV TINI_VERSION v0.16.1
RUN curl -L -o /usr/sbin/tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-$(dpkg --print-architecture) && \
chmod 0755 /usr/sbin/tini
ENTRYPOINT ["/usr/sbin/tini", "--"]
ENV XDG_CONFIG_HOME=/data
VOLUME /data
COPY context/duplicati-cli context/duplicati-server /usr/bin/
RUN chmod 0755 /usr/bin/duplicati-cli /usr/bin/duplicati-server
ARG CHANNEL=
ARG VERSION=
ENV DUPLICATI_CHANNEL=${CHANNEL}
ENV DUPLICATI_VERSION=${VERSION}
COPY duplicati /opt/duplicati
EXPOSE 8200
CMD ["/usr/bin/duplicati-server", "--webservice-port=8200", "--webservice-interface=any"]
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
EXE_FILE=/opt/duplicati/Duplicati.CommandLine.exe
APP_NAME=Duplicati.CommandLine
exec -a "$APP_NAME" mono "$EXE_FILE" "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
EXE_FILE=/opt/duplicati/Duplicati.Server.exe
APP_NAME=DuplicatiServer
exec -a "$APP_NAME" mono "$EXE_FILE" "$@"

Some files were not shown because too many files have changed in this diff Show More