Update Telegram SharpAESCrypt dep
Update SystemIOWindows to split the difference

# Conflicts:
#	BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj
#	BuildTools/AutoUpdateBuilder/packages.config
#	BuildTools/DependencyFinder/DependencyFinder.csproj
#	BuildTools/DependencyFinder/packages.config
#	BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
#	BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
#	BuildTools/UpdateVersionStamp/app.config
#	Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj
#	Duplicati/CommandLine/BackendTester/app.config
#	Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
#	Duplicati/CommandLine/BackendTool/app.config
#	Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj
#	Duplicati/CommandLine/Duplicati.CommandLine.csproj
#	Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
#	Duplicati/CommandLine/RecoveryTool/packages.config
#	Duplicati/CommandLine/app.config
#	Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
#	Duplicati/GUI/Duplicati.GUI.TrayIcon/app.config
#	Duplicati/GUI/Duplicati.GUI.TrayIcon/packages.config
#	Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj
#	Duplicati/Library/AutoUpdater/app.config
#	Duplicati/Library/AutoUpdater/packages.config
#	Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj
#	Duplicati/Library/Backend/AlternativeFTP/app.config
#	Duplicati/Library/Backend/AlternativeFTP/packages.config
#	Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj
#	Duplicati/Library/Backend/AzureBlob/packages.config
#	Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
#	Duplicati/Library/Backend/Backblaze/packages.config
#	Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj
#	Duplicati/Library/Backend/Box/packages.config
#	Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj
#	Duplicati/Library/Backend/CloudFiles/app.config
#	Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj
#	Duplicati/Library/Backend/Dropbox/packages.config
#	Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj
#	Duplicati/Library/Backend/FTP/app.config
#	Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj
#	Duplicati/Library/Backend/File/app.config
#	Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj
#	Duplicati/Library/Backend/GoogleServices/packages.config
#	Duplicati/Library/Backend/HubiC/Duplicati.Library.Backend.HubiC.csproj
#	Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
#	Duplicati/Library/Backend/Mega/Duplicati.Library.Backend.Mega.csproj
#	Duplicati/Library/Backend/Mega/packages.config
#	Duplicati/Library/Backend/OAuthHelper/Duplicati.Library.OAuthHelper.csproj
#	Duplicati/Library/Backend/OAuthHelper/packages.config
#	Duplicati/Library/Backend/OneDrive/Duplicati.Library.Backend.OneDrive.csproj
#	Duplicati/Library/Backend/OneDrive/packages.config
#	Duplicati/Library/Backend/OpenStack/Duplicati.Library.Backend.OpenStack.csproj
#	Duplicati/Library/Backend/OpenStack/packages.config
#	Duplicati/Library/Backend/Rclone/Duplicati.Library.Backend.Rclone.csproj
#	Duplicati/Library/Backend/Rclone/packages.config
#	Duplicati/Library/Backend/S3/Duplicati.Library.Backend.S3.csproj
#	Duplicati/Library/Backend/S3/app.config
#	Duplicati/Library/Backend/S3/packages.config
#	Duplicati/Library/Backend/SSHv2/Duplicati.Library.Backend.SSHv2.csproj
#	Duplicati/Library/Backend/SSHv2/packages.config
#	Duplicati/Library/Backend/SharePoint/Duplicati.Library.Backend.SharePoint.csproj
#	Duplicati/Library/Backend/SharePoint/app.config
#	Duplicati/Library/Backend/Sia/Duplicati.Library.Backend.Sia.csproj
#	Duplicati/Library/Backend/Sia/packages.config
#	Duplicati/Library/Backend/TahoeLAFS/Duplicati.Library.Backend.TahoeLAFS.csproj
#	Duplicati/Library/Backend/TahoeLAFS/packages.config
#	Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj
#	Duplicati/Library/Backend/Tardigrade/packages.config
#	Duplicati/Library/Backend/WEBDAV/Duplicati.Library.Backend.WEBDAV.csproj
#	Duplicati/Library/Backend/WEBDAV/app.config
#	Duplicati/Library/Common/Duplicati.Library.Common.csproj
#	Duplicati/Library/Common/IO/SystemIOWindows.cs
#	Duplicati/Library/Common/app.config
#	Duplicati/Library/Common/packages.config
#	Duplicati/Library/Compression/Duplicati.Library.Compression.csproj
#	Duplicati/Library/Compression/app.config
#	Duplicati/Library/Compression/packages.config
#	Duplicati/Library/DynamicLoader/Duplicati.Library.DynamicLoader.csproj
#	Duplicati/Library/DynamicLoader/app.config
#	Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj
#	Duplicati/Library/Encryption/packages.config
#	Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
#	Duplicati/Library/Localization/Duplicati.Library.Localization.csproj
#	Duplicati/Library/Localization/packages.config
#	Duplicati/Library/Logging/Duplicati.Library.Logging.csproj
#	Duplicati/Library/Logging/app.config
#	Duplicati/Library/Main/Duplicati.Library.Main.csproj
#	Duplicati/Library/Main/app.config
#	Duplicati/Library/Main/packages.config
#	Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj
#	Duplicati/Library/Modules/Builtin/packages.config
#	Duplicati/Library/SQLiteHelper/Duplicati.Library.SQLiteHelper.csproj
#	Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj
#	Duplicati/Library/Snapshots/app.config
#	Duplicati/Library/Snapshots/packages.config
#	Duplicati/Library/UsageReporter/Duplicati.Library.UsageReporter.csproj
#	Duplicati/Library/UsageReporter/packages.config
#	Duplicati/Library/Utility/Duplicati.Library.Utility.csproj
#	Duplicati/Library/Utility/app.config
#	Duplicati/Library/Utility/packages.config
#	Duplicati/License/Duplicati.License.csproj
#	Duplicati/Server/Duplicati.Server.Serialization/Duplicati.Server.Serialization.csproj
#	Duplicati/Server/Duplicati.Server.Serialization/packages.config
#	Duplicati/Server/Duplicati.Server.csproj
#	Duplicati/Server/app.config
#	Duplicati/Server/packages.config
#	Duplicati/Service/Duplicati.Service.csproj
#	Duplicati/Service/app.config
#	Duplicati/Tools/Duplicati.Tools.csproj
#	Duplicati/UnitTest/Duplicati.UnitTest.csproj
#	Duplicati/UnitTest/RestoreHandlerTests.cs
#	Duplicati/UnitTest/app.config
#	Duplicati/UnitTest/packages.config
#	Duplicati/WindowsService/App.config
#	Duplicati/WindowsService/WindowsService.csproj
#	Tools/SQLiteTool/App.config
#	Tools/SQLiteTool/SQLiteTool.csproj
#	thirdparty/TlsTest/TlsTest.csproj
#	thirdparty/UnixSupport/UnixSupport.csproj
#	thirdparty/mozroots/mozroots.csproj
This commit is contained in:
Thomas Suckow
2020-12-29 09:59:42 -08:00
315 changed files with 4791 additions and 1259 deletions
+3 -3
View File
@@ -57,19 +57,19 @@ jobs:
directories:
- $BUILD_DIR
script:
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink
- stage: tests
cache:
directories:
- $BUILD_DIR
script:
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border
- travis_wait ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border
- stage: tests
cache:
directories:
- $BUILD_DIR
script:
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler
- ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller
- stage: tests
cache:
+1 -1
View File
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/></startup></configuration>
+1 -1
View File
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/></startup></configuration>
+1 -1
View File
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/></startup></configuration>
+13 -1
View File
@@ -103,7 +103,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Encryptio
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Duplicati\CommandLine\AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{4D1B198B-F773-44F2-872B-2B283053323C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{AE035E01-C917-4F13-A35E-78F21C1A2F17}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{AE035E01-C917-4F13-A35E-78F21C1A2F17}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.TencentCOS", "Duplicati\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj", "{545DD6D4-9476-42D6-B51C-A28E000C489E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Telegram", "Duplicati\Library\Backend\Telegram\Duplicati.Library.Backend.Telegram.csproj", "{B6BB37DE-1DCA-401B-B92F-202F341916EA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -315,6 +319,14 @@ Global
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.Build.0 = Release|Any CPU
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.Build.0 = Release|Any CPU
{B6BB37DE-1DCA-401B-B92F-202F341916EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6BB37DE-1DCA-401B-B92F-202F341916EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6BB37DE-1DCA-401B-B92F-202F341916EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6BB37DE-1DCA-401B-B92F-202F341916EA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -39,6 +39,10 @@
<Project>{ae035e01-c917-4f13-a35e-78f21c1a2f17}</Project>
<Name>Duplicati.Library.Backend.Tardigrade</Name>
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Telegram\Duplicati.Library.Backend.Telegram.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
@@ -46,6 +46,12 @@
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\TahoeLAFS\Duplicati.Library.Backend.TahoeLAFS.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Telegram\Duplicati.Library.Backend.Telegram.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
+78 -13
View File
@@ -19,7 +19,9 @@ using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Utility;
namespace Duplicati.CommandLine
{
@@ -234,14 +236,82 @@ namespace Duplicati.CommandLine
}
}
public static List<string> PrefixArgsWithAsterisk(IList<string> argList)
/// <summary>
/// For bare file names with no wildcards, replace argument with
/// the equivalent of prefixing with "*/" so we search all folders.
/// </summary>
public static IEnumerable<string> PrefixArgsWithAsterisk(IEnumerable<string> argList) => argList.Select(PrefixArgWithAsterisk);
/// <summary>
/// For bare file names with no wildcards, return argument with
/// the equivalent of prefixing with "*/" so we search all folders.
/// </summary>
private static string PrefixArgWithAsterisk(string arg)
{
// Prefix all filenames with "*/" so we search all folders
var specialArgChars = new[] { '*', '?', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
return argList.Select(arg => arg.IndexOfAny(specialArgChars) < 0
&& !arg.StartsWith("[", StringComparison.Ordinal) ? "*" + Util.DirectorySeparatorString + arg : arg).ToList();
var containsSeparators = ContainsDirectorySeparators(arg);
var containsWildcards = ContainsWildcards(arg);
if (!containsSeparators && arg.StartsWith("@", StringComparison.Ordinal))
{
// Convert to Regexp filter and prefix with ".*/"
return $"[.*{Utility.ConvertLiteralToRegExp(Util.DirectorySeparatorString + arg.Substring(1))}]";
}
else if (!containsSeparators && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
{
// Prefix files with "*/"
return "*" + Util.DirectorySeparatorString + arg;
}
else
{
return arg;
}
}
/// <summary>
/// For folders, replace argument with the equivalent of suffixing
/// with "*" so we restore contents in the folder.
/// </summary>
public static IEnumerable<string> SuffixArgsWithAsterisk(IEnumerable<string> argList) => argList.Select(SuffixArgWithAsterisk);
/// <summary>
/// For folders, return argument with the equivalent of suffixing
/// with "*" so we restore contents in the folder.
/// </summary>
private static string SuffixArgWithAsterisk(string arg)
{
var containsWildcards = ContainsWildcards(arg);
var endsWithSeparator = arg.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal);
if (endsWithSeparator && arg.StartsWith("@", StringComparison.Ordinal))
{
// Convert to Regexp filter and suffix with ".*"
return $"[{Utility.ConvertLiteralToRegExp(arg.Substring(1))}.*]";
}
else if (endsWithSeparator && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
{
// Suffix with "*"
return arg + "*";
}
else
{
return arg;
}
}
private static readonly char[] pathSeparatorsCharacters = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
/// <summary>
/// Returns true if <paramref name="s"/> contains directory separator characters.
/// </summary>
public static bool ContainsDirectorySeparators(string s) => s.IndexOfAny(pathSeparatorsCharacters) >= 0;
private static readonly char[] wildcardCharacters = new[] { '*', '?' };
/// <summary>
/// Returns true if <paramref name="s"/> contains wildcard characters.
/// </summary>
public static bool ContainsWildcards(string s) => s.IndexOfAny(wildcardCharacters) >= 0;
public static int List(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
filter = filter ?? new Duplicati.Library.Utility.FilterExpression();
@@ -267,7 +337,7 @@ namespace Duplicati.CommandLine
options["version"] = v.ToString();
}
}
else if (args[0].IndexOfAny(new char[] { '*', '?' }) < 0 && !args[0].StartsWith("[", StringComparison.Ordinal))
else if (!ContainsWildcards(args[0]) && !args[0].StartsWith("[", StringComparison.Ordinal) && !args[0].StartsWith("@", StringComparison.Ordinal))
{
try
{
@@ -282,7 +352,7 @@ namespace Duplicati.CommandLine
}
}
args = PrefixArgsWithAsterisk(args);
args = PrefixArgsWithAsterisk(args).ToList();
// Support for not adding the --auth-username if possible
string dbpath;
@@ -448,12 +518,7 @@ namespace Duplicati.CommandLine
bool controlFiles = Library.Utility.Utility.ParseBoolOption(options, "control-files");
options.Remove("control-files");
args = PrefixArgsWithAsterisk(args);
// suffix all folders with "*" so we restore all contents in the folder
for (var ix = 0; ix < args.Count; ix++)
if (args[ix].IndexOfAny(new char[] { '*', '?' }) < 0 && !args[ix].StartsWith("[", StringComparison.Ordinal) && args[ix].EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal))
args[ix] += "*";
args = SuffixArgsWithAsterisk(PrefixArgsWithAsterisk(args)).ToList();
using(var output = new ConsoleOutput(outwriter, options))
using (var i = new Library.Main.Controller(backend, options, output))
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
</startup>
<runtime>
<AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false" />
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
@@ -36,6 +36,8 @@
</ProjectReference>
<ProjectReference Include="..\Library\Backend\Sia\Duplicati.Library.Backend.Sia.csproj">
</ProjectReference>
<ProjectReference Include="..\Library\Backend\Telegram\Duplicati.Library.Backend.Telegram.csproj">
</ProjectReference>
<ProjectReference Include="..\Library\Compression\Duplicati.Library.Compression.csproj">
</ProjectReference>
<ProjectReference Include="..\Library\Utility\Duplicati.Library.Utility.csproj">
@@ -74,7 +74,7 @@ namespace Duplicati.CommandLine.RecoveryTool
try
{
var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(file);
var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file));
if (p == null)
{
Console.WriteLine(" - Not a Duplicati file, ignoring");
+1 -1
View File
@@ -64,7 +64,7 @@ namespace Duplicati.CommandLine.RecoveryTool
try
{
var p = Library.Main.Volumes.VolumeBase.ParseFilename(file);
var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file));
if (p == null)
{
Console.WriteLine(" - Not a Duplicati file, ignoring");
+3 -3
View File
@@ -51,7 +51,7 @@ namespace Duplicati.CommandLine.RecoveryTool
{
var file = SelectListFile(args[2], folder);
var p = Library.Main.Volumes.VolumeBase.ParseFilename(file);
var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file));
Library.Main.Volumes.VolumeReaderBase.UpdateOptionsFromManifest(p.CompressionModule, file, new Duplicati.Library.Main.Options(options));
@@ -64,7 +64,7 @@ namespace Duplicati.CommandLine.RecoveryTool
public static IEnumerable<Duplicati.Library.Main.Volumes.IFileEntry> EnumerateFilesInDList(string file, Duplicati.Library.Utility.IFilter filter, Dictionary<string, string> options)
{
var p = Library.Main.Volumes.VolumeBase.ParseFilename(file);
var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file));
using (var fs = new System.IO.FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var cm = Library.DynamicLoader.CompressionLoader.GetModule(p.CompressionModule, fs, Library.Interface.ArchiveMode.Read, options))
using (var filesetreader = new Library.Main.Volumes.FilesetVolumeReader(cm, new Duplicati.Library.Main.Options(options)))
@@ -112,7 +112,7 @@ namespace Duplicati.CommandLine.RecoveryTool
{
return (
from v in Directory.EnumerateFiles(folder)
let p = Library.Main.Volumes.VolumeBase.ParseFilename(v)
let p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(v))
where p != null && p.FileType == Duplicati.Library.Main.RemoteVolumeType.Files
orderby p.Time descending
select new KeyValuePair<DateTime, string>(p.Time, v)).ToArray();
@@ -27,6 +27,8 @@ namespace Duplicati.CommandLine.RecoveryTool
{
public static class Restore
{
private static readonly ISystemIO systemIO = SystemIO.IO_OS;
public static int Run(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
if (args.Count != 2 && args.Count != 3)
@@ -160,8 +162,8 @@ namespace Duplicati.CommandLine.RecoveryTool
try
{
var targetfile = MapToRestorePath(f.Path, largestprefix, targetpath);
if (!Directory.Exists(Path.GetDirectoryName(targetfile)))
Directory.CreateDirectory(Path.GetDirectoryName(targetfile));
if (!systemIO.DirectoryExists(systemIO.PathGetDirectoryName(targetfile)))
systemIO.DirectoryCreate(systemIO.PathGetDirectoryName(targetfile));
Console.Write("{0}: {1} ({2})", i, targetfile, Library.Utility.Utility.FormatSizeString(f.Size));
@@ -216,12 +218,12 @@ namespace Duplicati.CommandLine.RecoveryTool
if (fh == f.Hash)
{
Console.WriteLine(" done!");
File.Copy(tf, targetfile, true);
systemIO.FileCopy(tf, targetfile, true);
}
else
{
Console.Write(" - Restored file hash mismatch");
if (File.Exists(targetfile))
if (systemIO.FileExists(targetfile))
Console.WriteLine(" - not overwriting existing file: {0}", targetfile);
else
Console.WriteLine(" - restoring file in damaged condition");
@@ -256,10 +258,10 @@ namespace Duplicati.CommandLine.RecoveryTool
if (path.Substring(1, 1) == ":")
prefixpath = path.Substring(0, 1) + path.Substring(2);
return Path.Combine(restorepath, prefixpath);
return systemIO.PathCombine(restorepath, prefixpath);
}
return Path.Combine(restorepath, path.Substring(prefixpath.Length));
return systemIO.PathCombine(restorepath, path.Substring(prefixpath.Length));
}
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Azure.KeyVault.Core" version="3.0.4" targetFramework="net462" />
<package id="Microsoft.Data.Edm" version="5.8.4" targetFramework="net462" />
<package id="Microsoft.Data.OData" version="5.8.4" targetFramework="net462" />
<package id="Microsoft.Data.Services.Client" version="5.8.4" targetFramework="net462" />
<package id="Microsoft.Rest.ClientRuntime" version="2.3.20" targetFramework="net462" />
<package id="Microsoft.Rest.ClientRuntime.Azure" version="3.3.19" targetFramework="net462" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net462" />
<package id="System.Spatial" version="5.8.4" targetFramework="net462" />
</packages>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /></startup>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" /></startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
@@ -614,13 +614,13 @@ namespace Duplicati.Library.Backend.OpenStack
authuris.AppendLine(string.Format("{0}: {1}", s.Key, s.Value));
return new List<ICommandLineArgument>(new [] {
new CommandLineArgument(DOMAINNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.DomainnameOptionShort, Strings.OpenStack.UsernameOptionLong),
new CommandLineArgument(DOMAINNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.DomainnameOptionShort, Strings.OpenStack.DomainnameOptionLong),
new CommandLineArgument(USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.UsernameOptionShort, Strings.OpenStack.UsernameOptionLong),
new CommandLineArgument(PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.PasswordOptionShort, Strings.OpenStack.PasswordOptionLong(TENANTNAME_OPTION)),
new CommandLineArgument(TENANTNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.TenantnameOptionShort, Strings.OpenStack.TenantnameOptionLong),
new CommandLineArgument(APIKEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.ApikeyOptionShort, Strings.OpenStack.ApikeyOptionLong),
new CommandLineArgument(AUTHURI_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.AuthuriOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())),
new CommandLineArgument(VERSION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.VersionOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())),
new CommandLineArgument(VERSION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.VersionOptionShort, Strings.OpenStack.VersionOptionLong),
new CommandLineArgument(REGION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.RegionOptionShort, Strings.OpenStack.RegionOptionLong),
});
}
+2 -2
View File
@@ -249,7 +249,7 @@ namespace Duplicati.Library.Backend
{
try
{
RcloneCommandExecuter(rclone_executable, String.Format("copyto {2}:{3}/{4} {0}:{1}", local_repo, filename, remote_repo, remote_path, remotename), CancellationToken.None).Await();
RcloneCommandExecuter(rclone_executable, String.Format("copyto {2}:{3} {0}:{1}", local_repo, filename, remote_repo, Path.Combine(this.remote_path, remotename)), CancellationToken.None).Await();
}
catch (FolderMissingException ex) {
throw new FileMissingException(ex);
@@ -262,7 +262,7 @@ namespace Duplicati.Library.Backend
// Will give a "directory not found" error if the file does not exist, need to change that to a missing file exception
try
{
RcloneCommandExecuter(rclone_executable, String.Format("delete {0}:{1}/{2}", remote_repo, remote_path, remotename), CancellationToken.None).Await();
RcloneCommandExecuter(rclone_executable, String.Format("delete {0}:{1}", remote_repo, Path.Combine(remote_path, remotename)), CancellationToken.None).Await();
}
catch (FolderMissingException ex) {
throw new FileMissingException(ex);
+5 -21
View File
@@ -51,6 +51,7 @@ namespace Duplicati.Library.Backend
{ "dinCloud - Chicago", "d3-ord.dincloud.com" },
{ "dinCloud - Los Angeles", "d3-lax.dincloud.com" },
{ "IBM COS (S3) Public US", "s3-api.us-geo.objectstorage.softlayer.net" },
{ "Storadera", "eu-east-1.s3.storadera.com" },
{ "Wasabi Hot Storage", "s3.wasabisys.com" },
{ "Wasabi Hot Storage (US West)", "s3.us-west-1.wasabisys.com" },
{ "Wasabi Hot Storage (EU Central)", "s3.eu-central-1.wasabisys.com" },
@@ -65,6 +66,7 @@ namespace Duplicati.Library.Backend
{ "Europe (EU, London)", "eu-west-2" },
{ "Europe (EU, Paris)", "eu-west-3" },
{ "Europe (EU, Stockholm)", "eu-north-1" },
{ "Europe (EU, Milan)", "eu-south-1" },
{ "US East (Northern Virginia)", "us-east-1" },
{ "US East (Ohio)", "us-east-2" },
{ "US West (Northern California)", "us-west-1" },
@@ -90,6 +92,7 @@ namespace Duplicati.Library.Backend
{ "eu-west-2", "s3.eu-west-2.amazonaws.com" },
{ "eu-west-3", "s3.eu-west-3.amazonaws.com" },
{ "eu-north-1", "s3.eu-north-1.amazonaws.com" },
{ "eu-south-1", "s3.eu-south-1.amazonaws.com" },
{ "eu-central-1", "s3.eu-central-1.amazonaws.com" },
{ "us-east-1", "s3.amazonaws.com" },
{ "us-east-2", "s3.us-east-2.amazonaws.com" },
@@ -105,7 +108,7 @@ namespace Duplicati.Library.Backend
{ "sa-east-1", "s3.sa-east-1.amazonaws.com" },
{ "cn-north-1", "s3.cn-north-1.amazonaws.com.cn" },
{ "cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn" },
{ "me-south-1", "s3.me-south-1.amazonaws.com.cn" },
{ "me-south-1", "s3.me-south-1.amazonaws.com" },
};
public static readonly Dictionary<string, string> KNOWN_S3_STORAGE_CLASSES;
@@ -353,26 +356,7 @@ namespace Duplicati.Library.Backend
public void Get(string remotename, System.IO.Stream output)
{
try
{
Connection.GetFileStream(m_bucket, GetFullKey(remotename), output);
}
catch
{
//This is a fix for the S3 backend prior to beta 3, where the filenames had a slash prefixed
try
{
if (!remotename.StartsWith("/", StringComparison.Ordinal))
Connection.GetFileStream(m_bucket, GetFullKey("/" + remotename), output);
return;
}
catch
{
}
//Throw original error
throw;
}
Connection.GetFileStream(m_bucket, GetFullKey(remotename), output);
}
public void Delete(string remotename)
+6 -1
View File
@@ -83,8 +83,13 @@ namespace Duplicati.Library.Backend.Sia
private string getResponseBodyOnError(string context, System.Net.WebException wex)
{
HttpWebResponse response = wex.Response as HttpWebResponse;
if (response is null)
{
return $"{context} failed with error: {wex.Message}";
}
string body = "";
System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)wex.Response;
using (System.IO.Stream data = response.GetResponseStream())
using (var reader = new System.IO.StreamReader(data))
{
@@ -18,7 +18,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="uplink.NET" Version="2.1.5" />
<PackageReference Include="uplink.NET" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -23,6 +24,7 @@ namespace Duplicati.Library.Backend.Tardigrade
private const string TARDIGRADE_BUCKET = "tardigrade-bucket";
private const string TARDIGRADE_FOLDER = "tardigrade-folder";
private const string PROTOCOL_KEY = "tardigrade";
private const string TARDIGRADE_PARTNER_ID = "duplicati";
private readonly string _satellite;
private readonly string _api_key;
@@ -36,7 +38,9 @@ namespace Duplicati.Library.Backend.Tardigrade
public static readonly Dictionary<string, string> KNOWN_TARDIGRADE_SATELLITES = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){
{ "US Central 1", "us-central-1.tardigrade.io:7777" },
{ "Asia East 1", "asia-east-1.tardigrade.io:7777" },
{ "Saltlake", "saltlake.tardigrade.io:7777" },
{ "Europe West 1", "europe-west-1.tardigrade.io:7777" },
{ "Europe North 1", "europe-north-1.tardigrade.io:7777" },
};
public static readonly Dictionary<string, string> KNOWN_AUTHENTICATION_METHODS = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){
@@ -44,6 +48,30 @@ namespace Duplicati.Library.Backend.Tardigrade
{ "Access grant", "Access grant" },
};
[DllImport("kernel32.dll")]
protected static extern IntPtr LoadLibrary(string filename);
private static bool _libraryLoaded = false;
private static void InitStorjLibrary()
{
if (_libraryLoaded)
return;
if (Duplicati.Library.Common.Platform.IsClientWindows) //We need to init only on Windows to distinguish between x64 and x86
{
if (System.Environment.Is64BitProcess)
{
var res = LoadLibrary("win-x64/storj_uplink.dll");
}
else
{
var res = LoadLibrary("win-x86/storj_uplink.dll");
}
}
Access.SetTempDirectory(Library.Utility.TempFolder.SystemTempPath);
_libraryLoaded = true;
}
// ReSharper disable once UnusedMember.Global
// This constructor is needed by the BackendLoader.
public Tardigrade()
@@ -54,15 +82,14 @@ namespace Duplicati.Library.Backend.Tardigrade
// This constructor is needed by the BackendLoader.
public Tardigrade(string url, Dictionary<string, string> options)
{
InitStorjLibrary();
var auth_method = options[TARDIGRADE_AUTH_METHOD];
Access.SetTempDirectory(System.IO.Path.GetTempPath());
if (auth_method == "Access grant")
{
//Create an access from the access grant
var shared_access = options[TARDIGRADE_SHARED_ACCESS];
_access = new Access(shared_access);
_access = new Access(shared_access, new Config() { UserAgent = TARDIGRADE_PARTNER_ID });
}
else
{
@@ -78,7 +105,7 @@ namespace Duplicati.Library.Backend.Tardigrade
_secret = options[TARDIGRADE_SECRET];
}
_access = new Access(_satellite, _api_key, _secret);
_access = new Access(_satellite, _api_key, _secret, new Config() { UserAgent = TARDIGRADE_PARTNER_ID });
}
_bucketService = new BucketService(_access);
@@ -241,7 +268,10 @@ namespace Duplicati.Library.Backend.Tardigrade
foreach (var obj in objects.Items)
{
TardigradeFile file = new TardigradeFile(obj);
file.Name = file.Name.Replace(GetBasePath(), "");
if (prefix != "")
{
file.Name = file.Name.Replace(prefix, "");
}
files.Add(file);
}
@@ -0,0 +1,86 @@
using System;
using Duplicati.Library.Common.IO;
using TeleSharp.TL;
namespace Duplicati.Library.Backend
{
public class ChannelFileInfo : IEquatable<ChannelFileInfo>
{
public int MessageId { get; }
public long MediaDocAccessHash { get; }
public long DocumentId { get; }
public int Version { get; }
public long Size { get; }
public string Name { get; }
public DateTime Date { get; }
public ChannelFileInfo()
{ }
public ChannelFileInfo(int messageId, long mediaDocAccessHash, long documentId, int version, long size, string name, DateTime date)
{
MessageId = messageId;
MediaDocAccessHash = mediaDocAccessHash;
DocumentId = documentId;
Version = version;
Size = size;
Name = name;
Date = date;
}
public FileEntry ToFileEntry()
{
return new FileEntry(Name, Size)
{
LastModification = Date,
LastAccess = Date,
IsFolder = false
};
}
public TLInputDocumentFileLocation ToFileLocation()
{
return new TLInputDocumentFileLocation
{
Id = DocumentId,
Version = Version,
AccessHash = MediaDocAccessHash
};
}
public bool Equals(ChannelFileInfo other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return MessageId == other.MessageId;
}
public override bool Equals(object obj)
{
return ReferenceEquals(this, obj) || obj is ChannelFileInfo other && Equals(other);
}
public override int GetHashCode()
{
return MessageId;
}
public static bool operator ==(ChannelFileInfo left, ChannelFileInfo right)
{
return Equals(left, right);
}
public static bool operator !=(ChannelFileInfo left, ChannelFileInfo right)
{
return !Equals(left, right);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Library</OutputType>
<RootNamespace>Duplicati.Library.Backend</RootNamespace>
<AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
<UseMSBuildEngine>false</UseMSBuildEngine>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\Logging\Duplicati.Library.Logging.csproj" />
<ProjectReference Include="..\..\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj" />
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj" />
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\..\Common\Duplicati.Library.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpAESCrypt.dll" Version="1.3.4" />
<PackageReference Include="TLSharp" Version="0.1.0.574" />
</ItemGroup>
</Project>
Binary file not shown.
@@ -0,0 +1,159 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Duplicati.Library.AutoUpdater;
using TLSharp.Core;
namespace Duplicati.Library.Backend
{
public class EncryptedFileSessionStore : ISessionStore
{
private static readonly object m_lockObj = new object();
private static readonly uint[] m_lookup32 = CreateLookup32();
private readonly string m_teleDataPath;
private readonly string m_password;
private readonly SHA256 m_sha = SHA256.Create();
private static readonly ConcurrentDictionary<string, byte[]> m_userIdLastSessionMap = new ConcurrentDictionary<string, byte[]>();
public EncryptedFileSessionStore(string password)
{
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentNullException(nameof(password));
}
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appName = AutoUpdateSettings.AppName;
m_teleDataPath = Path.Combine(appData, appName, nameof(Telegram));
m_password = password;
Directory.CreateDirectory(m_teleDataPath);
}
public void Save(Session session)
{
if (session.AuthKey == null)
{
return;
}
var sessionId = session.SessionUserId;
var filePath = GetSessionFilePath(sessionId);
var sessionBytes = session.ToBytes();
if (m_userIdLastSessionMap.TryGetValue(sessionId, out var sessionCache))
{
if (sessionCache.SequenceEqual(sessionBytes))
{
return;
}
}
WriteToEncryptedStorage(sessionBytes, m_password, filePath, sessionId);
}
public Session Load(string userId)
{
if (m_userIdLastSessionMap.TryGetValue(userId, out var cachedBytes))
{
var cachedSession = Session.FromBytes(cachedBytes, this, userId);
return cachedSession;
}
var filePath = GetSessionFilePath(userId);
var sessionBytes = ReadFromEncryptedStorage(m_password, filePath);
if (sessionBytes == null)
{
return null;
}
var session = Session.FromBytes(sessionBytes, this, userId);
return session;
}
private string GetSessionFilePath(string userId)
{
userId = userId.TrimStart('+');
var sha = GetShortSha(userId);
var sessionFilePath = Path.Combine(m_teleDataPath, $"t_{sha}.dat");
return sessionFilePath;
}
private static void WriteToEncryptedStorage(byte[] bytesToWrite, string pass, string path, string sessionId)
{
lock (m_lockObj)
{
using (var sessionMs = new MemoryStream(bytesToWrite))
using (var file = File.Open(path, FileMode.Create, FileAccess.Write))
{
SharpAESCrypt.SharpAESCrypt.Encrypt(pass, sessionMs, file);
}
m_userIdLastSessionMap[sessionId] = bytesToWrite;
}
}
private byte[] ReadFromEncryptedStorage(string pass, string path)
{
var fileInfo = new FileInfo(path);
if (fileInfo.Exists == false || fileInfo.Length == 0)
{
return null;
}
lock (m_lockObj)
{
using (var sessionMs = new MemoryStream())
using (var file = File.Open(path, FileMode.Open, FileAccess.Read))
{
SharpAESCrypt.SharpAESCrypt.Decrypt(pass, file, sessionMs);
return sessionMs.ToArray();
}
}
}
private string GetShortSha(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var longShaBytes = m_sha.ComputeHash(inputBytes);
var longSha = ByteArrayToHexViaLookup32(longShaBytes);
var result = longSha.Substring(0, 16);
return result;
}
private static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}
private static string ByteArrayToHexViaLookup32(byte[] bytes)
{
var lookup32 = m_lookup32;
var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
}
}
@@ -0,0 +1,131 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using TLSharp.Core;
using TLSharp.Core.Network;
namespace Duplicati.Library.Backend.Extensions
{
public static class TelegramClientExtensions
{
private static readonly BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Default;
public static async Task WrapperConnectAsync(this TelegramClient client, CancellationToken cancelToken = default(CancellationToken))
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
var oldTransportField = client.GetTcpTransport();
var tcpClient = oldTransportField.GetTcpClient();
var handler = client.GetTcpClientConnectionHandler();
var remoteEndpoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint;
var newTransport = new TcpTransport(remoteEndpoint.Address.ToString(), remoteEndpoint.Port, handler);
client.GetTcpTransportFieldInfo().SetValue(client, newTransport);
cancelToken.ThrowIfCancellationRequested();
await client.ConnectAsync(false, cancelToken);
cancelToken.ThrowIfCancellationRequested();
oldTransportField.Dispose();
}
public static bool IsReallyConnected(this TelegramClient client)
{
if (client.IsConnected == false)
{
return false;
}
var sender = GetMtProtoSender(client);
if (sender == null)
{
return false;
}
var tcpTransport = client.GetTcpTransport();
if (tcpTransport == null || tcpTransport.IsConnected == false)
{
return false;
}
var tcpClient = tcpTransport.GetTcpClient();
if (tcpClient == null || tcpClient.Connected == false)
{
return false;
}
return true;
}
private static TcpClientConnectionHandler GetTcpClientConnectionHandler(this TelegramClient client)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
var handlerFieldInfo = typeof(TelegramClient).GetField("handler", _bindingFlags);
var handler = (TcpClientConnectionHandler)handlerFieldInfo.GetValue(client);
return handler;
}
private static TcpClient GetTcpClient(this TcpTransport tcpTransport)
{
if (tcpTransport == null)
{
throw new ArgumentNullException(nameof(tcpTransport));
}
var tcpClientFieldInfo = typeof(TcpTransport).GetField("tcpClient", _bindingFlags);
var tcpClient = (TcpClient)tcpClientFieldInfo.GetValue(tcpTransport);
return tcpClient;
}
private static FieldInfo GetTcpTransportFieldInfo(this TelegramClient client)
{
var transportFieldInfo = typeof(TelegramClient).GetField("transport", _bindingFlags);
return transportFieldInfo;
}
private static TcpTransport GetTcpTransport(this TelegramClient client)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
var transportFieldInfo = client.GetTcpTransportFieldInfo();
var oldTransportField = (TcpTransport)transportFieldInfo.GetValue(client);
return oldTransportField;
}
private static MtProtoSender GetMtProtoSender(this TelegramClient client)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
var senderFieldInfo = typeof(TelegramClient).GetField("sender", _bindingFlags);
var sender = (MtProtoSender)senderFieldInfo.GetValue(client);
return sender;
}
}
}
@@ -0,0 +1,53 @@
#region Disclaimer / License
// 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 it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Telegram")]
[assembly: AssemblyDescription("A Telegram backend for Duplicati")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Duplicati Team")]
[assembly: AssemblyProduct("Duplicati.Backend.Telegram")]
[assembly: AssemblyCopyright("LGPL, Copyright © Duplicati Team 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("afa68988-4d82-490e-8044-acbc7d52ef4f")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("2.0.0.7")]
[assembly: AssemblyFileVersion("2.0.0.7")]
@@ -0,0 +1,48 @@
using System.IO;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Backend
{
/// <summary>
/// Private helper class to fix a bug with the StreamReader
/// </summary>
internal class StreamReadHelper : OverrideableStream
{
/// <summary>
/// Once the stream has returned 0 as the read count it is disposed,
/// and subsequent read requests will throw an ObjectDisposedException
/// </summary>
private bool m_empty;
/// <summary>
/// Basic initialization, just pass the stream to the super class
/// </summary>
/// <param name="stream"></param>
public StreamReadHelper(Stream stream) : base(stream)
{ }
/// <summary>
/// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted
/// </summary>
/// <param name="buffer">The buffer to place data in</param>
/// <param name="offset">The offset into the buffer to start at</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>The number of bytes read</returns>
public override int Read(byte[] buffer, int offset, int count)
{
var readCount = 0;
int a;
while (!m_empty && count > 0)
{
a = base.Read(buffer, offset, count);
readCount += a;
count -= a;
offset += a;
m_empty = a == 0;
}
return readCount;
}
}
}
@@ -0,0 +1,63 @@
using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Backend
{
internal static class Strings
{
#region Constants
public const string API_ID_KEY = "api-id";
public const string API_HASH_KEY = "api-hash";
public const string PHONE_NUMBER_KEY = "phone-number";
public const string AUTH_CODE_KEY = "auth-code";
public const string AUTH_PASSWORD = "auth-password";
public const string CHANNEL_NAME = "channel-name";
#endregion
#region General
public static string DisplayName { get; } = "Telegram";
public static string Description => LC.L("This backend can read and write data to a Telegram backend");
#endregion
#region Errors
public static string WrongAuthCodeError => LC.L("The auth code is incorrect");
public static string NoAuthCodeError => LC.L("The auth code is missing");
public static string NoPasswordError => LC.L("The password is missing");
public static string NoChannelNameError => LC.L("The channel name is missing");
public static string NoApiIdError => LC.L("The API ID is missing");
public static string NoApiHashError => LC.L("The API hash is missing");
public static string NoPhoneNumberError => LC.L("The phone number is missing");
#endregion
#region Descriptions
public static string ApiIdShort => LC.L("The API ID");
public static string ApiHashShort => LC.L("The API hash");
public static string ApiIdLong => LC.L("The API ID retrieved from https://my.telegram.org/");
public static string ApiHashLong => LC.L("The API hash retrieved from https://my.telegram.org/");
public static string PhoneNumberShort => LC.L("Your phone number");
public static string PhoneNumberLong => LC.L("The phone number you registered with");
public static string AuthCodeShort => LC.L("The code you should have received (if you did)");
public static string AuthCodeLong => LC.L("The auth code that you received. Input only if you did receive it");
public static string PasswordShort => LC.L("2FA password (if enabled)");
public static string PasswordLong => LC.L("The 2 step verification password. Input only if you have set it up");
public static string ChannelName => LC.L("The channel name of the backup");
#endregion
#region Formats
public const string TELEGRAM_FLOOD = "It's required to wait {0} seconds before continuing";
public const string STARTING_EXECUTING = "Starting executing action {0}";
public const string DONE_EXECUTING = "Done executing action {0}";
public const string USER_INFO_EXC = "Exception thrown that should be shown on UI";
#endregion
}
}
@@ -0,0 +1,512 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Backend.Extensions;
using Duplicati.Library.Interface;
using Duplicati.Library.Logging;
using TeleSharp.TL;
using TeleSharp.TL.Channels;
using TeleSharp.TL.Messages;
using TLSharp.Core;
using TLSharp.Core.Exceptions;
using TLSharp.Core.Network.Exceptions;
using TLSharp.Core.Utils;
using TLRequestDeleteMessages = TeleSharp.TL.Channels.TLRequestDeleteMessages;
namespace Duplicati.Library.Backend
{
public class Telegram : IStreamingBackend
{
private readonly TelegramClient m_telegramClient;
private readonly EncryptedFileSessionStore m_encSessionStore;
private static readonly object m_lockObj = new object();
private readonly int m_apiId;
private readonly string m_apiHash;
private readonly string m_authCode;
private readonly string m_password;
private readonly string m_channelName;
private readonly string m_phoneNumber;
private TLChannel m_channelCache;
private static string m_phoneCodeHash;
private static readonly string m_logTag = Log.LogTagFromType(typeof(Telegram));
private const int BYTES_IN_MEBIBYTE = 1048576;
public Telegram()
{ }
public Telegram(string url, Dictionary<string, string> options)
{
if (options.TryGetValue(Strings.API_ID_KEY, out var apiId))
{
m_apiId = int.Parse(apiId);
}
if (options.TryGetValue(Strings.API_HASH_KEY, out var apiHash))
{
m_apiHash = apiHash.Trim();
}
if (options.TryGetValue(Strings.PHONE_NUMBER_KEY, out var phoneNumber))
{
m_phoneNumber = phoneNumber.Trim();
}
if (options.TryGetValue(Strings.AUTH_CODE_KEY, out var authCode))
{
m_authCode = authCode.Trim();
}
if (options.TryGetValue(Strings.AUTH_PASSWORD, out var password))
{
m_password = password.Trim();
}
if (options.TryGetValue(Strings.CHANNEL_NAME, out var channelName))
{
m_channelName = channelName.Trim();
}
if (m_apiId == 0)
{
throw new UserInformationException(Strings.NoApiIdError, nameof(Strings.NoApiIdError));
}
if (string.IsNullOrEmpty(m_apiHash))
{
throw new UserInformationException(Strings.NoApiHashError, nameof(Strings.NoApiHashError));
}
if (string.IsNullOrEmpty(m_phoneNumber))
{
throw new UserInformationException(Strings.NoPhoneNumberError, nameof(Strings.NoPhoneNumberError));
}
if (string.IsNullOrEmpty(m_channelName))
{
throw new UserInformationException(Strings.NoChannelNameError, nameof(Strings.NoChannelNameError));
}
m_encSessionStore = new EncryptedFileSessionStore($"{m_apiHash}_{m_apiId}");
m_telegramClient = new TelegramClient(m_apiId, m_apiHash, m_encSessionStore, m_phoneNumber);
}
public void Dispose()
{
m_telegramClient?.Dispose();
}
public string DisplayName { get; } = Strings.DisplayName;
public string ProtocolKey { get; } = "telegram";
public IEnumerable<IFileEntry> List()
{
return SafeExecute<IEnumerable<IFileEntry>>(() =>
{
Authenticate();
EnsureChannelCreated();
var fileInfos = ListChannelFileInfos();
var result = fileInfos.Select(fi => fi.ToFileEntry());
return result;
},
nameof(List));
}
public Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
{
SafeExecute(() =>
{
cancelToken.ThrowIfCancellationRequested();
Authenticate();
cancelToken.ThrowIfCancellationRequested();
var channel = GetChannel();
cancelToken.ThrowIfCancellationRequested();
using (var sr = new StreamReader(new StreamReadHelper(stream)))
{
cancelToken.ThrowIfCancellationRequested();
EnsureConnected(cancelToken);
cancelToken.ThrowIfCancellationRequested();
var file = m_telegramClient.UploadFile(remotename, sr, cancelToken).GetAwaiter().GetResult();
cancelToken.ThrowIfCancellationRequested();
var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = (long)channel.AccessHash};
var fileNameAttribute = new TLDocumentAttributeFilename
{
FileName = remotename
};
EnsureConnected(cancelToken);
m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector<TLAbsDocumentAttribute> {fileNameAttribute}, cancelToken).GetAwaiter().GetResult();
}
},
nameof(PutAsync));
return Task.CompletedTask;
}
public void Get(string remotename, Stream stream)
{
SafeExecute(() =>
{
var fileInfo = ListChannelFileInfos().First(fi => fi.Name == remotename);
var fileLocation = fileInfo.ToFileLocation();
var limit = BYTES_IN_MEBIBYTE;
var currentOffset = 0;
while (currentOffset < fileInfo.Size)
{
try
{
EnsureConnected();
var file = m_telegramClient.GetFile(fileLocation, limit, currentOffset).GetAwaiter().GetResult();
stream.Write(file.Bytes, 0, file.Bytes.Length);
currentOffset += file.Bytes.Length;
}
catch (InvalidOperationException e)
{
if (e.Message.Contains("Couldn't read the packet length") == false)
{
throw;
}
}
}
},
$"{nameof(Get)}");
}
public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
{
using (var fs = File.OpenRead(filename))
{
PutAsync(remotename, fs, cancelToken).GetAwaiter().GetResult();
}
return Task.CompletedTask;
}
public void Get(string remotename, string filename)
{
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
Get(remotename, fs);
}
}
public void Delete(string remotename)
{
SafeExecute(() =>
{
var channel = GetChannel();
var fileInfo = ListChannelFileInfos().FirstOrDefault(fi => fi.Name == remotename);
if (fileInfo == null)
{
return;
}
var request = new TLRequestDeleteMessages
{
Channel = new TLInputChannel
{
ChannelId = channel.Id,
AccessHash = channel.AccessHash.Value
},
Id = new TLVector<int> {fileInfo.MessageId}
};
EnsureConnected();
m_telegramClient.SendRequestAsync<TLAffectedMessages>(request).GetAwaiter().GetResult();
},
$"{nameof(Delete)}({remotename})");
}
public List<ChannelFileInfo> ListChannelFileInfos()
{
var channel = GetChannel();
var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = channel.AccessHash.Value};
var result = new List<ChannelFileInfo>();
var oldMinDate = 0L;
var newMinDate = (long?)null;
while (oldMinDate != newMinDate)
{
oldMinDate = newMinDate ?? 0L;
RetrieveMessages(inputPeerChannel, result, oldMinDate);
if (result.Any() == false)
{
break;
}
newMinDate = result.Min(cfi => Utility.Utility.NormalizeDateTimeToEpochSeconds(cfi.Date));
}
result = result.Distinct().ToList();
return result;
}
private void RetrieveMessages(TLInputPeerChannel inputPeerChannel, List<ChannelFileInfo> result, long offsetDate)
{
EnsureConnected();
var absHistory = m_telegramClient.GetHistoryAsync(inputPeerChannel, offsetDate: (int)offsetDate).GetAwaiter().GetResult();
var history = ((TLChannelMessages)absHistory).Messages.OfType<TLMessage>();
foreach (var msg in history)
{
if (msg.Media is TLMessageMediaDocument media &&
media.Document is TLDocument mediaDoc)
{
var fileInfo = new ChannelFileInfo(
msg.Id,
mediaDoc.AccessHash,
mediaDoc.Id,
mediaDoc.Version,
mediaDoc.Size,
media.Caption,
DateTimeOffset.FromUnixTimeSeconds(msg.Date).UtcDateTime);
result.Add(fileInfo);
}
}
}
public IList<ICommandLineArgument> SupportedCommands { get; } = new List<ICommandLineArgument>
{
new CommandLineArgument(Strings.API_ID_KEY, CommandLineArgument.ArgumentType.Integer, Strings.ApiIdShort, Strings.ApiIdLong),
new CommandLineArgument(Strings.API_HASH_KEY, CommandLineArgument.ArgumentType.String, Strings.ApiHashShort, Strings.ApiHashLong),
new CommandLineArgument(Strings.PHONE_NUMBER_KEY, CommandLineArgument.ArgumentType.String, Strings.PhoneNumberShort, Strings.PhoneNumberLong),
new CommandLineArgument(Strings.AUTH_CODE_KEY, CommandLineArgument.ArgumentType.String, Strings.AuthCodeShort, Strings.AuthCodeLong),
new CommandLineArgument(Strings.AUTH_PASSWORD, CommandLineArgument.ArgumentType.String, Strings.PasswordShort, Strings.PasswordLong),
new CommandLineArgument(Strings.CHANNEL_NAME, CommandLineArgument.ArgumentType.String, Strings.ChannelName, Strings.ChannelName)
};
public string Description { get; } = Strings.Description;
public string[] DNSName { get; }
public void Test()
{
SafeExecute(Authenticate, nameof(Authenticate));
}
public void CreateFolder()
{
SafeExecute(() =>
{
Authenticate();
EnsureChannelCreated();
},
nameof(CreateFolder));
}
private TLChannel GetChannel()
{
if (m_channelCache != null)
{
return m_channelCache;
}
var absChats = GetChats();
var channel = (TLChannel)absChats.FirstOrDefault(chat => chat is TLChannel tlChannel && tlChannel.Title == m_channelName);
m_channelCache = channel;
return channel;
}
private IEnumerable<TLAbsChat> GetChats()
{
var lastDate = 0;
while (true)
{
EnsureConnected();
var dialogs = m_telegramClient.GetUserDialogsAsync(lastDate).GetAwaiter().GetResult();
var tlDialogs = dialogs as TLDialogs;
var tlDialogsSlice = dialogs as TLDialogsSlice;
foreach (var chat in tlDialogs?.Chats ?? tlDialogsSlice?.Chats)
{
switch (chat)
{
case TLChannelForbidden _:
case TLChatForbidden _:
break;
case TLChat c:
lastDate = c.Date;
break;
case TLChannel c:
lastDate = c.Date;
break;
default:
throw new NotSupportedException($"Unsupported chat type {chat.GetType()}");
}
yield return chat;
}
if (tlDialogs?.Dialogs?.Count < 100 || tlDialogsSlice?.Dialogs?.Count < 100)
{
yield break;
}
}
}
private void EnsureChannelCreated()
{
var channel = GetChannel();
if (channel == null)
{
var newGroup = new TLRequestCreateChannel
{
Broadcast = false,
Megagroup = false,
Title = m_channelName,
About = string.Empty
};
EnsureConnected();
m_telegramClient.SendRequestAsync<object>(newGroup).GetAwaiter().GetResult();
}
}
private void Authenticate()
{
EnsureConnected();
if (IsAuthenticated())
{
return;
}
try
{
if (m_phoneCodeHash == null)
{
EnsureConnected();
var phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult();
SetPhoneCodeHash(phoneCodeHash);
m_telegramClient.Session.Save();
if (string.IsNullOrEmpty(m_authCode))
{
throw new UserInformationException(Strings.NoAuthCodeError, nameof(Strings.NoAuthCodeError));
}
throw new UserInformationException(Strings.WrongAuthCodeError, nameof(Strings.WrongAuthCodeError));
}
m_telegramClient.MakeAuthAsync(m_phoneNumber, m_phoneCodeHash, m_authCode).GetAwaiter().GetResult();
}
catch (CloudPasswordNeededException)
{
if (string.IsNullOrEmpty(m_password))
{
m_telegramClient.Session.Save();
throw new UserInformationException(Strings.NoPasswordError, nameof(Strings.NoPasswordError));
}
EnsureConnected();
var passwordSetting = m_telegramClient.GetPasswordSetting().GetAwaiter().GetResult();
m_telegramClient.MakeAuthWithPasswordAsync(passwordSetting, m_password).GetAwaiter().GetResult();
}
m_telegramClient.Session.Save();
}
private void EnsureConnected(CancellationToken cancelToken = default(CancellationToken))
{
if (m_telegramClient.IsReallyConnected())
{
return;
}
cancelToken.ThrowIfCancellationRequested();
m_telegramClient.WrapperConnectAsync(cancelToken).GetAwaiter().GetResult();
cancelToken.ThrowIfCancellationRequested();
if (m_telegramClient.IsReallyConnected() == false)
{
throw new WebException("Unable to connect to telegram");
}
}
private bool IsAuthenticated()
{
var isAuthorized = m_telegramClient.IsUserAuthorized();
return isAuthorized;
}
private static void SetPhoneCodeHash(string phoneCodeHash)
{
m_phoneCodeHash = phoneCodeHash;
}
private void SafeExecute(Action action, string actionName)
{
lock (m_lockObj)
{
Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
try
{
action();
}
catch (UserInformationException uiExc)
{
Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC);
throw;
}
catch (FloodException floodExc)
{
var randSeconds = new Random().Next(2, 15);
Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
Thread.Sleep(floodExc.TimeToWait + TimeSpan.FromSeconds(randSeconds));
SafeExecute(action, actionName);
}
}
Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
}
private T SafeExecute<T>(Func<T> func, string actionName)
{
lock (m_lockObj)
{
Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
try
{
var res = func();
Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
return res;
}
catch (UserInformationException uiExc)
{
Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC);
throw;
}
catch (FloodException floodExc)
{
var randSeconds = new Random().Next(2, 15);
Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
Thread.Sleep(floodExc.TimeToWait);
var res = SafeExecute(func, actionName);
Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
return res;
}
}
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/>
</startup>
</configuration>
@@ -0,0 +1,411 @@
using COSXML;
using COSXML.Auth;
using COSXML.Model.Bucket;
using COSXML.Model.Object;
using COSXML.Model.Tag;
using COSXML.Utils;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Duplicati.Library.Backend.TencentCOS
{
/// <summary>
/// Tencent COS
/// https://cloud.tencent.com/document/product/436
/// https://cloud.tencent.com/document/product/436/32869
/// </summary>
public class COS : IBackend, IStreamingBackend, IRenameEnabledBackend
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType<COS>();
private const string COS_APP_ID = "cos-app-id";
private const string COS_REGION = "cos-region";
private const string COS_SECRET_ID = "cos-secret-id";
private const string COS_SECRET_KEY = "cos-secret-key";
private const string COS_BUCKET = "cos-bucket";
/// <summary>
/// Set default HTTPS request
/// </summary>
private const bool COS_USE_SSL = true;
/// <summary>
/// Connection timeout time, in milliseconds, default 45000ms
/// </summary>
private const int CONNECTION_TIMEOUT_MS = 1000 * 60 * 60;
/// <summary>
/// Read and write timeout time, in milliseconds, default 45000ms
/// </summary>
private const int READ_WRITE_TIMEOUT_MS = 1000 * 60 * 60;
/// <summary>
/// The valid time of each request signature, unit: second
/// </summary>
private const int KEY_DURATION_SECOND = 60 * 60;
private readonly CosOptions _cosOptions;
CosXml cosXml;
/// <summary>
/// Tencent Cloud Object Storage Configuration
/// </summary>
public class CosOptions
{
/// <summary>
/// Tencent Cloud Account APPID
/// </summary>
public string Appid { get; set; }
/// <summary>
/// Cloud API Secret Id
/// </summary>
public string SecretId { get; set; }
/// <summary>
/// Cloud API Secret Key
/// </summary>
public string SecretKey { get; set; }
/// <summary>
/// Bucket region ap-guangzhou ap-hongkong
/// https://cloud.tencent.com/document/product/436/6224
/// </summary>
public string Region { get; set; }
/// <summary>
/// Bucket, format: BucketName-APPID
/// </summary>
public string Bucket { get; set; }
/// <summary>
/// A path or subfolder in a bucket
/// </summary>
public string Path { get; set; }
}
public COS() { }
public COS(string url, Dictionary<string, string> options)
{
_cosOptions = new CosOptions();
var uri = new Utility.Uri(url?.Trim());
var prefix = uri.HostAndPath?.Trim()?.Trim('/')?.Trim('\\');
if (!string.IsNullOrEmpty(prefix))
{
_cosOptions.Path = prefix + "/";
}
if (options.ContainsKey(COS_APP_ID))
{
_cosOptions.Appid = options[COS_APP_ID];
}
if (options.ContainsKey(COS_REGION))
{
_cosOptions.Region = options[COS_REGION];
}
if (options.ContainsKey(COS_SECRET_ID))
{
_cosOptions.SecretId = options[COS_SECRET_ID];
}
if (options.ContainsKey(COS_SECRET_KEY))
{
_cosOptions.SecretKey = options[COS_SECRET_KEY];
}
if (options.ContainsKey(COS_BUCKET))
{
_cosOptions.Bucket = options[COS_BUCKET];
}
}
CosXml GetCosXml()
{
string appid = _cosOptions.Appid;
string region = _cosOptions.Region;
string secretId = _cosOptions.SecretId;
string secretKey = _cosOptions.SecretKey;
CosXmlConfig config = new CosXmlConfig.Builder()
.SetConnectionTimeoutMs(CONNECTION_TIMEOUT_MS)
.SetReadWriteTimeoutMs(READ_WRITE_TIMEOUT_MS)
.IsHttps(COS_USE_SSL)
.SetAppid(appid)
.SetRegion(region)
.Build();
// Initialization QCloudCredentialProvider, COS SDK provides three ways: the permanent temporary keys custom
QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, KEY_DURATION_SECOND);
return new CosXmlServer(config, cosCredentialProvider);
}
public IEnumerable<IFileEntry> List()
{
bool isTruncated = true;
string filename = null;
while (isTruncated)
{
cosXml = GetCosXml();
string bucket = _cosOptions.Bucket;
string prefix = _cosOptions.Path;
GetBucketRequest request = new GetBucketRequest(bucket);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600);
if (!string.IsNullOrEmpty(filename))
{
request.SetMarker(filename);
}
if (!string.IsNullOrEmpty(prefix))
{
request.SetPrefix(prefix);
}
GetBucketResult result = cosXml.GetBucket(request);
ListBucket info = result.listBucket;
isTruncated = result.listBucket.isTruncated;
filename = result.listBucket.nextMarker;
foreach (var item in info.contentsList)
{
var last = DateTime.Parse(item.lastModified);
var fileName = item.key;
if (!string.IsNullOrWhiteSpace(prefix))
{
fileName = fileName.Substring(prefix.Length);
if (fileName.StartsWith("/", StringComparison.Ordinal))
{
fileName = fileName.Trim('/');
}
}
yield return new FileEntry(fileName, item.size, last, last);
}
}
}
public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
{
using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
return PutAsync(remotename, fs, cancelToken);
}
public void Get(string remotename, string filename)
{
using (var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None))
Get(remotename, fs);
}
public void Delete(string remotename)
{
try
{
cosXml = GetCosXml();
string bucket = _cosOptions.Bucket;
string key = GetFullKey(remotename);
DeleteObjectRequest request = new DeleteObjectRequest(bucket, key);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600);
DeleteObjectResult result = cosXml.DeleteObject(request);
}
catch (COSXML.CosException.CosClientException clientEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Delete", clientEx, "Delete failed: {0}", remotename);
throw;
}
catch (COSXML.CosException.CosServerException serverEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Delete", serverEx, "Delete failed: {0}, {1}", remotename, serverEx.GetInfo());
throw;
}
}
public void Test()
{
var json = JsonConvert.SerializeObject(_cosOptions);
try
{
cosXml = GetCosXml();
string bucket = _cosOptions.Bucket;
HeadBucketRequest request = new HeadBucketRequest(bucket);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600);
HeadBucketResult result = cosXml.HeadBucket(request);
Logging.Log.WriteInformationMessage(LOGTAG, "Test", "Request complete {0}: {1}, {2}", result.httpCode, json, result.GetResultInfo());
}
catch (COSXML.CosException.CosClientException clientEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Test", clientEx, "Request failed: {0}", json);
throw;
}
catch (COSXML.CosException.CosServerException serverEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Test", serverEx, "Request failed: {0}, {1}", json, serverEx.GetInfo());
throw;
}
}
public void CreateFolder()
{
// No need to create folders
}
public void Dispose()
{
if (cosXml != null)
{
cosXml = null;
}
}
public Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
{
try
{
cosXml = GetCosXml();
string bucket = _cosOptions.Bucket;
string key = GetFullKey(remotename);
byte[] buffer = new byte[stream.Length];
if (Utility.Utility.ForceStreamRead(stream, buffer, buffer.Length) != stream.Length)
{
throw new Exception("Bad file read");
}
PutObjectRequest request = new PutObjectRequest(bucket, key, buffer);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND);
request.SetRequestHeader("Content-Type", "application/octet-stream");
PutObjectResult result = cosXml.PutObject(request);
}
catch (COSXML.CosException.CosClientException clientEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "PutAsync", clientEx, "Put failed: {0}", remotename);
throw;
}
catch (COSXML.CosException.CosServerException serverEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "PutAsync", serverEx, "Put failed: {0}, {1}", remotename, serverEx.GetInfo());
throw;
}
return Task.CompletedTask;
}
public void Get(string remotename, Stream stream)
{
try
{
cosXml = GetCosXml();
string bucket = _cosOptions.Bucket;
string key = GetFullKey(remotename);
GetObjectBytesRequest request = new GetObjectBytesRequest(bucket, key);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND);
GetObjectBytesResult result = cosXml.GetObject(request);
byte[] bytes = result.content;
Stream ms = new MemoryStream(bytes);
Utility.Utility.CopyStream(ms, stream);
}
catch (COSXML.CosException.CosClientException clientEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Get", clientEx, "Get failed: {0}", remotename);
throw;
}
catch (COSXML.CosException.CosServerException serverEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Get", serverEx, "Get failed: {0}, {1}", remotename, serverEx.GetInfo());
throw;
}
}
public void Rename(string oldname, string newname)
{
try
{
cosXml = GetCosXml();
string sourceAppid = _cosOptions.Appid;
string sourceBucket = _cosOptions.Bucket;
string sourceRegion = _cosOptions.Region;
string sourceKey = GetFullKey(oldname);
CopySourceStruct copySource = new CopySourceStruct(sourceAppid, sourceBucket,
sourceRegion, sourceKey);
string bucket = _cosOptions.Bucket;
string key = GetFullKey(newname);
CopyObjectRequest request = new CopyObjectRequest(bucket, key);
request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND);
request.SetCopySource(copySource);
request.SetCopyMetaDataDirective(COSXML.Common.CosMetaDataDirective.COPY);
CopyObjectResult result = cosXml.CopyObject(request);
//Console.WriteLine(result.GetResultInfo());
Delete(oldname);
}
catch (COSXML.CosException.CosClientException clientEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Rename", clientEx, "Rename failed: {0} to {1}", oldname, newname);
throw;
}
catch (COSXML.CosException.CosServerException serverEx)
{
Logging.Log.WriteErrorMessage(LOGTAG, "Rename", serverEx, "Rename failed: {0} to {1}, {2}", oldname, newname, serverEx.GetInfo());
throw;
}
}
public string DisplayName => Strings.COSBackend.DisplayName;
public string Description => Strings.COSBackend.Description;
public string ProtocolKey => "cos";
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
new CommandLineArgument(COS_APP_ID, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSAccountDescriptionShort,Strings.COSBackend.COSAccountDescriptionLong),
new CommandLineArgument(COS_REGION, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSLocationDescriptionShort,Strings.COSBackend.COSLocationDescriptionLong),
new CommandLineArgument(COS_SECRET_ID, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSAPISecretIdDescriptionShort,Strings.COSBackend.COSAPISecretIdDescriptionLong),
new CommandLineArgument(COS_SECRET_KEY, CommandLineArgument.ArgumentType.Password, Strings.COSBackend.COSAPISecretKeyDescriptionShort,Strings.COSBackend.COSAPISecretKeyDescriptionLong),
new CommandLineArgument(COS_BUCKET, CommandLineArgument.ArgumentType.String,Strings.COSBackend.COSBucketDescriptionShort,Strings.COSBackend.COSBucketDescriptionLong),
});
}
}
public string[] DNSName => null;
private string GetFullKey(string name)
{
if (string.IsNullOrWhiteSpace(_cosOptions?.Path))
{
return name;
}
return _cosOptions.Path + name;
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj" />
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\..\Logging\Duplicati.Library.Logging.csproj" />
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.11" />
</ItemGroup>
</Project>
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("Duplicati.Library.Backend.TencentCOS")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Duplicati.Library.Backend.TencentCOS")]
[assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("545dd6d4-9476-42d6-b51c-a28e000c489e")]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,26 @@
using Duplicati.Library.Localization.Short;
namespace Duplicati.Library.Backend.Strings
{
internal static class COSBackend
{
public static string DisplayName { get { return LC.L(@"Tencent COS"); } }
public static string Description { get { return LC.L(@"Object storage (Cloud Object Storage, COS) is a distributed storage service for storing massive files provided by Tencent Cloud, which has the advantages of high scalability, low cost, reliability and security."); } }
public static string COSAccountDescriptionShort { get { return LC.L(@"Account ID"); } }
public static string COSAccountDescriptionLong { get { return LC.L(@"Account ID of Tencent Cloud Account"); } }
public static string COSAPISecretIdDescriptionShort { get { return LC.L(@"Secret Id"); } }
public static string COSAPISecretIdDescriptionLong { get { return LC.L(@"Cloud API Secret Id"); } }
public static string COSAPISecretKeyDescriptionShort { get { return LC.L(@"Secret Key"); } }
public static string COSAPISecretKeyDescriptionLong { get { return LC.L(@"Cloud API Secret Key"); } }
public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket"); } }
public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket, format: BucketName-APPID"); } }
public static string COSLocationDescriptionShort { get { return LC.L(@"Specifies COS location constraints"); } }
public static string COSLocationDescriptionLong { get { return LC.L(@"Region (Region) is the distribution area of the Tencent cloud hosting machine room, the object storage COS data is stored in the storage buckets of these regions. https://cloud.tencent.com/document/product/436/6224"); } }
}
}
+3
View File
@@ -29,7 +29,9 @@ namespace Duplicati.Library.Common.IO
{
IFileEntry DirectoryEntry(string path);
void DirectoryCreate(string path);
void DirectoryDelete(string path, bool recursive);
bool DirectoryExists(string path);
void DirectoryMove(string sourceDirName, string destDirName);
void DirectorySetLastWriteTimeUtc(string path, DateTime time);
void DirectorySetCreationTimeUtc(string path, DateTime time);
@@ -64,6 +66,7 @@ namespace Duplicati.Library.Common.IO
DateTime GetLastWriteTimeUtc(string path);
IEnumerable<string> EnumerateFileSystemEntries(string path);
IEnumerable<string> EnumerateFiles(string path);
IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption);
IEnumerable<string> EnumerateDirectories(string path);
void SetMetadata(string path, Dictionary<string, string> metdata, bool restorePermissions);
@@ -32,11 +32,21 @@ namespace Duplicati.Library.Common.IO
Directory.CreateDirectory(NormalizePath(path));
}
public void DirectoryDelete(string path, bool recursive)
{
Directory.Delete(NormalizePath(path), recursive);
}
public bool DirectoryExists(string path)
{
return Directory.Exists(NormalizePath(path));
}
public void DirectoryMove(string sourceDirName, string destDirName)
{
System.IO.Directory.Move(NormalizePath(sourceDirName), NormalizePath(destDirName));
}
public void FileDelete(string path)
{
File.Delete(path);
@@ -117,6 +127,10 @@ namespace Duplicati.Library.Common.IO
return Directory.EnumerateFiles(path);
}
public IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption)
{
return Directory.EnumerateFiles(path, searchPattern, searchOption);
}
public string PathGetFileName(string path)
{
+134 -210
View File
@@ -34,28 +34,48 @@ namespace Duplicati.Library.Common.IO
private const string PATHPREFIX_SERVER = @"\\";
private static readonly string DIRSEP = Util.DirectorySeparatorString;
private static bool IsPathTooLong(string path)
{
// Use 258 for length check instead of 260 (MAX_PATH) - we need to leave room for the 16-bit (wide) null terminator
return path.StartsWith(UNCPREFIX, StringComparison.Ordinal) || path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal) || path.Length > 258;
}
public static string PrefixWithUNC(string path)
{
if (path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal))
if (IsPrefixedWithUNC(path))
{
return path;
if (path.StartsWith(UNCPREFIX, StringComparison.Ordinal))
return path;
}
return path.StartsWith(PATHPREFIX_SERVER, StringComparison.Ordinal)
? UNCPREFIX_SERVER + path.Remove(0, PATHPREFIX_SERVER.Length)
? UNCPREFIX_SERVER + path.Substring(PATHPREFIX_SERVER.Length)
: UNCPREFIX + path;
}
private static bool IsPrefixedWithUNC(string path)
{
return path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal) ||
path.StartsWith(UNCPREFIX, StringComparison.Ordinal);
}
public static string StripUNCPrefix(string path)
{
return path.StartsWith(UNCPREFIX, StringComparison.Ordinal) ? path.Substring(UNCPREFIX.Length) : path;
if (path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal))
{
// @"\\?\UNC\example.com\share\file.txt" to @"\\example.com\share\file.txt"
return PATHPREFIX_SERVER + path.Substring(UNCPREFIX_SERVER.Length);
}
else if (path.StartsWith(UNCPREFIX, StringComparison.Ordinal))
{
// @"\\?\C:\file.txt" to @"C:\file.txt"
return path.Substring(UNCPREFIX.Length);
}
else
{
return path;
}
}
/// <summary>
/// Convert forward slashes to backslashes.
/// </summary>
/// <returns>Path with forward slashes replaced by backslashes.</returns>
private static string ConvertSlashes(string path)
{
return path.Replace("/", Util.DirectorySeparatorString);
}
private class FileSystemAccess
@@ -139,175 +159,100 @@ namespace Duplicati.Library.Common.IO
private System.Security.AccessControl.FileSystemSecurity GetAccessControlDir(string path)
{
if (!IsPathTooLong(path))
try
{
var di = new DirectoryInfo(path);
return di.GetAccessControl();
}
catch (System.IO.PathTooLongException) { }
catch (System.ArgumentException) { }
//TODO-DNC Revert back to proper call when Library will be netstandard ready
return (System.Security.AccessControl.FileSystemSecurity)
typeof(Alphaleonis.Win32.Filesystem.Directory).GetMethod("GetAccessControl", new Type[] { typeof(string) })
.Invoke(null, new object[] { PrefixWithUNC(path) });
return new DirectoryInfo(PrefixWithUNC(path)).GetAccessControl();
}
private System.Security.AccessControl.FileSystemSecurity GetAccessControlFile(string path)
{
if (!IsPathTooLong(path))
try
{
var fi = new FileInfo(path);
return fi.GetAccessControl();
}
catch (System.IO.PathTooLongException) { }
catch (System.ArgumentException) { }
//TODO-DNC Revert back to proper call when Library will be netstandard ready
return (System.Security.AccessControl.FileSystemSecurity)
typeof(Alphaleonis.Win32.Filesystem.File).GetMethod("GetAccessControl", new Type[] { typeof(string) })
.Invoke(null, new object[] { PrefixWithUNC(path) });
return new FileInfo(PrefixWithUNC(path)).GetAccessControl();
}
private void SetAccessControlFile(string path, FileSecurity rules)
{
if (!IsPathTooLong(path))
try
{
new FileInfo(path).SetAccessControl(rules);
return;
}
catch (System.IO.PathTooLongException) { }
catch (System.ArgumentException) { }
//TODO-DNC Revert back to proper call when Library will be netstandard ready
typeof(Alphaleonis.Win32.Filesystem.File).GetMethod("SetAccessControl", new Type[] { typeof(string), typeof(System.Security.AccessControl.FileSystemSecurity), typeof(AccessControlSections) })
.Invoke(null, new object[] { PrefixWithUNC(path), rules, AccessControlSections.All });
new FileInfo(PrefixWithUNC(path)).SetAccessControl(rules);
}
private void SetAccessControlDir(string path, DirectorySecurity rules)
{
if (!IsPathTooLong(path))
try
{
new DirectoryInfo(path).SetAccessControl(rules);
return;
}
catch (System.IO.PathTooLongException) { }
catch (System.ArgumentException) { }
//TODO-DNC Revert back to proper call when Library will be netstandard ready
typeof(Alphaleonis.Win32.Filesystem.Directory).GetMethod("SetAccessControl", new Type[] { typeof(string), typeof(System.Security.AccessControl.FileSystemSecurity), typeof(AccessControlSections) })
.Invoke(null, new object[] { PrefixWithUNC(path), rules, AccessControlSections.All });
}
private static void PathTooLongVoidFuncWrapper<U, T>(Func<string, T> nativeIOFunc,
Func<string, U> alternativeIOFunc,
string path, bool prefixWithUnc = false)
{
// Wrap void into bool return type to avoid code duplication. Code at anytime available for replacement.
PathTooLongFuncWrapper(p => { nativeIOFunc(p); return true; }, p => { alternativeIOFunc(p); return true; } , path, prefixWithUnc);
}
private static void PathTooLongActionWrapper(Action<string> nativeIOFunc,
Action<string> alternativeIOFunc,
string path, bool prefixWithUnc = false)
{
// Wrap void into bool return type to avoid code duplication. Code at anytime available for replacement.
PathTooLongFuncWrapper(p => { nativeIOFunc(p); return true; }, p => { alternativeIOFunc(p); return true; }, path, prefixWithUnc);
}
private static T PathTooLongFuncWrapper<T>(Func<string, T> nativeIOFunc,
Func<string, T> alternativeIOFunc,
string path, bool prefixWithUnc = false)
{
if (!IsPathTooLong(path))
try { return nativeIOFunc(path); }
catch (System.IO.PathTooLongException) { }
catch (System.ArgumentException) { }
return !prefixWithUnc ? alternativeIOFunc(path) : alternativeIOFunc(PrefixWithUNC(path));
new DirectoryInfo(PrefixWithUNC(path)).SetAccessControl(rules);
}
#region ISystemIO implementation
public void DirectoryCreate(string path)
{
PathTooLongVoidFuncWrapper(System.IO.Directory.CreateDirectory,
Alphaleonis.Win32.Filesystem.Directory.CreateDirectory, path, true);
System.IO.Directory.CreateDirectory(PrefixWithUNC(path));
}
public void DirectoryDelete(string path, bool recursive)
{
System.IO.Directory.Delete(PrefixWithUNC(path), recursive);
}
public bool DirectoryExists(string path)
{
return PathTooLongFuncWrapper(System.IO.Directory.Exists,
Alphaleonis.Win32.Filesystem.Directory.Exists, path, true);
return System.IO.Directory.Exists(PrefixWithUNC(path));
}
public void DirectoryMove(string sourceDirName, string destDirName)
{
System.IO.Directory.Move(PrefixWithUNC(sourceDirName), PrefixWithUNC(destDirName));
}
public void FileDelete(string path)
{
PathTooLongActionWrapper(System.IO.File.Delete,
Alphaleonis.Win32.Filesystem.File.Delete, path, true);
System.IO.File.Delete(PrefixWithUNC(path));
}
public void FileSetLastWriteTimeUtc(string path, DateTime time)
{
PathTooLongActionWrapper(p => System.IO.File.SetLastWriteTimeUtc(p, time),
p => Alphaleonis.Win32.Filesystem.File.SetLastWriteTimeUtc(p, time), path, true);
System.IO.File.SetLastWriteTimeUtc(PrefixWithUNC(path), time);
}
public void FileSetCreationTimeUtc(string path, DateTime time)
{
PathTooLongActionWrapper(p => System.IO.File.SetCreationTimeUtc(p, time),
p => Alphaleonis.Win32.Filesystem.File.SetCreationTimeUtc(p, time), path, true);
System.IO.File.SetCreationTimeUtc(PrefixWithUNC(path), time);
}
public DateTime FileGetLastWriteTimeUtc(string path)
{
return PathTooLongFuncWrapper(System.IO.File.GetLastWriteTimeUtc,
Alphaleonis.Win32.Filesystem.File.GetLastWriteTimeUtc, path, true);
return System.IO.File.GetLastWriteTimeUtc(PrefixWithUNC(path));
}
public DateTime FileGetCreationTimeUtc(string path)
{
return PathTooLongFuncWrapper(System.IO.File.GetCreationTimeUtc,
Alphaleonis.Win32.Filesystem.File.GetCreationTimeUtc, path, true);
return System.IO.File.GetCreationTimeUtc(PrefixWithUNC(path));
}
public bool FileExists(string path)
{
return PathTooLongFuncWrapper(System.IO.File.Exists,
Alphaleonis.Win32.Filesystem.File.Exists, path, true);
return System.IO.File.Exists(PrefixWithUNC(path));
}
public System.IO.FileStream FileOpenRead(string path)
{
return PathTooLongFuncWrapper(p => System.IO.File.Open(p, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite),
p => Alphaleonis.Win32.Filesystem.File.Open(p, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite),
path, true);
return System.IO.File.Open(PrefixWithUNC(path), System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite);
}
public System.IO.FileStream FileOpenWrite(string path)
{
return !FileExists(path)
? FileCreate(path)
: PathTooLongFuncWrapper(System.IO.File.OpenWrite, Alphaleonis.Win32.Filesystem.File.OpenWrite, path, true);
: System.IO.File.OpenWrite(PrefixWithUNC(path));
}
public System.IO.FileStream FileCreate(string path)
{
return PathTooLongFuncWrapper(System.IO.File.Create, Alphaleonis.Win32.Filesystem.File.Create, path, true);
return System.IO.File.Create(PrefixWithUNC(path));
}
public System.IO.FileAttributes GetFileAttributes(string path)
{
return PathTooLongFuncWrapper(System.IO.File.GetAttributes, AlphaFS.File.GetAttributes, path, true);
return System.IO.File.GetAttributes(PrefixWithUNC(path));
}
public void SetFileAttributes(string path, System.IO.FileAttributes attributes)
{
PathTooLongActionWrapper(p => System.IO.File.SetAttributes(p, attributes),
p => Alphaleonis.Win32.Filesystem.File.SetAttributes(p, attributes), path, true);
System.IO.File.SetAttributes(PrefixWithUNC(path), attributes);
}
/// <summary>
@@ -319,7 +264,7 @@ namespace Duplicati.Library.Common.IO
{
try
{
return PathTooLongFuncWrapper(AlphaFS.File.GetLinkTargetInfo, AlphaFS.File.GetLinkTargetInfo, file, true).PrintName;
return AlphaFS.File.GetLinkTargetInfo(PrefixWithUNC(file)).PrintName;
}
catch (AlphaFS.NotAReparsePointException) { }
catch (AlphaFS.UnrecognizedReparsePointException) { }
@@ -332,156 +277,159 @@ namespace Duplicati.Library.Common.IO
public IEnumerable<string> EnumerateFileSystemEntries(string path)
{
return PathTooLongFuncWrapper(System.IO.Directory.EnumerateFileSystemEntries,
Alphaleonis.Win32.Filesystem.Directory.GetFileSystemEntries,
path, true).Select(StripUNCPrefix).AsEnumerable();
return System.IO.Directory.EnumerateFileSystemEntries(PrefixWithUNC(path)).Select(StripUNCPrefix);
}
public IEnumerable<string> EnumerateFiles(string path)
{
return PathTooLongFuncWrapper(System.IO.Directory.EnumerateFiles,
Alphaleonis.Win32.Filesystem.Directory.GetFiles, path,
true).Select(StripUNCPrefix).AsEnumerable();
return System.IO.Directory.EnumerateFiles(PrefixWithUNC(path)).Select(StripUNCPrefix);
}
public IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption)
{
return System.IO.Directory.EnumerateFiles(PrefixWithUNC(path), searchPattern, searchOption).Select(StripUNCPrefix);
}
public string PathGetFileName(string path)
{
return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetFileName,
Alphaleonis.Win32.Filesystem.Path.GetFileName,
path, true));
return StripUNCPrefix(System.IO.Path.GetFileName(PrefixWithUNC(path)));
}
public string PathGetDirectoryName(string path)
{
return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetDirectoryName,
Alphaleonis.Win32.Filesystem.Path.GetDirectoryName,
path, true));
return StripUNCPrefix(System.IO.Path.GetDirectoryName(PrefixWithUNC(path)));
}
public string PathGetExtension(string path)
{
return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetExtension,
Alphaleonis.Win32.Filesystem.Path.GetExtension,
path, true));
return StripUNCPrefix(System.IO.Path.GetExtension(PrefixWithUNC(path)));
}
public string PathChangeExtension(string path, string extension)
{
return StripUNCPrefix(PathTooLongFuncWrapper(p => System.IO.Path.ChangeExtension(p, extension),
p => Alphaleonis.Win32.Filesystem.Path.ChangeExtension(p, extension),
path, true));
return StripUNCPrefix(System.IO.Path.ChangeExtension(PrefixWithUNC(path), extension));
}
public void DirectorySetLastWriteTimeUtc(string path, DateTime time)
{
PathTooLongActionWrapper(p => System.IO.Directory.SetLastWriteTimeUtc(p, time),
p => Alphaleonis.Win32.Filesystem.File.SetLastWriteTimeUtc(p, time), path, true);
System.IO.Directory.SetLastWriteTimeUtc(PrefixWithUNC(path), time);
}
public void DirectorySetCreationTimeUtc(string path, DateTime time)
{
PathTooLongActionWrapper(p => System.IO.Directory.SetCreationTimeUtc(p, time),
p => Alphaleonis.Win32.Filesystem.File.SetCreationTimeUtc(p, time), path, true);
System.IO.Directory.SetCreationTimeUtc(PrefixWithUNC(path), time);
}
public void FileMove(string source, string target)
{
// We do not check if path is too long on target. If so, then we catch an exception.
PathTooLongActionWrapper(p => System.IO.File.Move(p, target),
p => Alphaleonis.Win32.Filesystem.File.Move(p, PrefixWithUNC(target)),
source, true);
System.IO.File.Move(PrefixWithUNC(source), PrefixWithUNC(target));
}
public long FileLength(string path)
{
return PathTooLongFuncWrapper(p => new System.IO.FileInfo(p).Length,
p => new Alphaleonis.Win32.Filesystem.FileInfo(p).Length,
path, true);
return new System.IO.FileInfo(PrefixWithUNC(path)).Length;
}
public string GetPathRoot(string path)
{
return PathTooLongFuncWrapper(Path.GetPathRoot, AlphaFS.Path.GetPathRoot, path, false);
if (IsPrefixedWithUNC(path))
{
return Path.GetPathRoot(path);
}
else
{
return StripUNCPrefix(Path.GetPathRoot(PrefixWithUNC(path)));
}
}
public string[] GetDirectories(string path)
{
return PathTooLongFuncWrapper(Directory.GetDirectories, AlphaFS.Directory.GetDirectories, path, false);
if (IsPrefixedWithUNC(path))
{
return Directory.GetDirectories(path);
}
else
{
return Directory.GetDirectories(PrefixWithUNC(path)).Select(StripUNCPrefix).ToArray();
}
}
public string[] GetFiles(string path)
{
return PathTooLongFuncWrapper(Directory.GetFiles, AlphaFS.Directory.GetFiles, path, false);
if (IsPrefixedWithUNC(path))
{
return Directory.GetFiles(path);
}
else
{
return Directory.GetFiles(PrefixWithUNC(path)).Select(StripUNCPrefix).ToArray();
}
}
public string[] GetFiles(string path, string searchPattern)
{
return PathTooLongFuncWrapper(p => Directory.GetFiles(p, searchPattern), p => AlphaFS.Directory.GetFiles(p, searchPattern), path, false);
if (IsPrefixedWithUNC(path))
{
return Directory.GetFiles(path, searchPattern);
}
else
{
return Directory.GetFiles(PrefixWithUNC(path), searchPattern).Select(StripUNCPrefix).ToArray();
}
}
public DateTime GetCreationTimeUtc(string path)
{
return PathTooLongFuncWrapper(Directory.GetCreationTimeUtc, AlphaFS.File.GetCreationTimeUtc, path, false);
return Directory.GetCreationTimeUtc(PrefixWithUNC(path));
}
public DateTime GetLastWriteTimeUtc(string path)
{
return PathTooLongFuncWrapper(Directory.GetLastWriteTimeUtc, AlphaFS.File.GetLastWriteTimeUtc, path, false);
return Directory.GetLastWriteTimeUtc(PrefixWithUNC(path));
}
public IEnumerable<string> EnumerateDirectories(string path)
{
return PathTooLongFuncWrapper(Directory.EnumerateDirectories, AlphaFS.Directory.EnumerateDirectories, path, false);
if (IsPrefixedWithUNC(path))
{
return Directory.EnumerateDirectories(path);
}
else
{
return Directory.EnumerateDirectories(PrefixWithUNC(path)).Select(StripUNCPrefix);
}
}
public void FileCopy(string source, string target, bool overwrite)
{
// We do not check if path is too long on target. If so, then we catch an exception.
PathTooLongActionWrapper(p => File.Copy(p, target, overwrite), p => AlphaFS.File.Copy(p, target, overwrite), source, false);
File.Copy(PrefixWithUNC(source), PrefixWithUNC(target), overwrite);
}
public string PathGetFullPath(string path)
{
return PathTooLongFuncWrapper(System.IO.Path.GetFullPath, AlphaFS.Path.GetFullPath, path, false);
if (IsPrefixedWithUNC(path))
{
return System.IO.Path.GetFullPath(ConvertSlashes(path));
}
else
{
return StripUNCPrefix(System.IO.Path.GetFullPath(PrefixWithUNC(ConvertSlashes(path))));
}
}
public IFileEntry DirectoryEntry(string path)
{
IFileEntry DirectoryEntryNative(string p) {
var dInfo = new DirectoryInfo(p);
return new FileEntry(dInfo.Name, 0, dInfo.LastAccessTime, dInfo.LastWriteTime)
{
IsFolder = true
};
}
IFileEntry DirectoryEntryAlphaFS(string p)
var dInfo = new DirectoryInfo(PrefixWithUNC(path));
return new FileEntry(dInfo.Name, 0, dInfo.LastAccessTime, dInfo.LastWriteTime)
{
var dInfoAlphaFS = new AlphaFS.DirectoryInfo(p);
return new FileEntry(dInfoAlphaFS.Name, 0, dInfoAlphaFS.LastAccessTime, dInfoAlphaFS.LastWriteTime)
{
IsFolder = true
};
}
return PathTooLongFuncWrapper(DirectoryEntryNative, DirectoryEntryAlphaFS, path, false);
IsFolder = true
};
}
public IFileEntry FileEntry(string path)
{
IFileEntry FileEntryNative(string p)
{
var fileInfo = new FileInfo(p);
return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime);
}
IFileEntry FileEntryAlphaFS(string p)
{
var fInfoAlphaFS = new AlphaFS.FileInfo(p);
return new FileEntry(fInfoAlphaFS.Name, fInfoAlphaFS.Length, fInfoAlphaFS.LastAccessTime, fInfoAlphaFS.LastWriteTime);
}
return PathTooLongFuncWrapper(FileEntryNative, FileEntryAlphaFS, path, false);
var fileInfo = new FileInfo(PrefixWithUNC(path));
return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime);
}
public Dictionary<string, string> GetMetadata(string path, bool isSymlink, bool followSymlink)
@@ -562,31 +510,7 @@ namespace Duplicati.Library.Common.IO
public string PathCombine(params string[] paths)
{
var combinedPath = "";
for (int i = 0; i < paths.Length; i++)
{
if (i == 0)
{
combinedPath = paths[i];
}
else
{
if (!IsPathTooLong(combinedPath + "\\" + paths[i]))
{
try
{
combinedPath = Path.Combine(combinedPath, paths[i]);
}
catch (Exception ex) when (ex is System.IO.PathTooLongException || ex is System.ArgumentException)
{
//TODO: Explain why we need to keep prefixing and stripping UNC's.
combinedPath = StripUNCPrefix(Alphaleonis.Win32.Filesystem.Path.Combine(PrefixWithUNC(combinedPath), paths[i]));
}
}
}
}
return combinedPath;
return Path.Combine(paths);
}
public void CreateSymlink(string symlinkfile, string target, bool asDir)
+15 -2
View File
@@ -25,6 +25,7 @@ using Duplicati.Library.Utility;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Common;
using Duplicati.Library.Interface;
using Duplicati.Library.Main.Database;
namespace Duplicati.Library.Main
{
@@ -205,8 +206,6 @@ namespace Duplicati.Library.Main
return RunAction(new ListRemoteResults(), (result) =>
{
result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Listing);
using (var tf = System.IO.File.Exists(m_options.Dbpath) ? null : new Library.Utility.TempFile())
using (var db = new Database.LocalDatabase(((string)tf) ?? m_options.Dbpath, "list-remote", true))
using (var bk = new BackendManager(m_backend, m_options, result.BackendWriter, null))
{
// Only delete files that match the expected pattern and prefix
@@ -216,6 +215,20 @@ namespace Duplicati.Library.Main
.Where(x => x.Prefix == m_options.Prefix)
.ToList();
// If the local database is available, we will use it to avoid deleting unrelated files
// from the backend. Otherwise, we may accidentally delete non-Duplicati files, or
// files from a different Duplicati configuration that points to the same backend location
// and uses the same prefix (see issues #2678, #3845, and #4244).
if (System.IO.File.Exists(m_options.Dbpath))
{
using (LocalDatabase db = new LocalDatabase(m_options.Dbpath, "list-remote", true))
{
IEnumerable<RemoteVolumeEntry> dbRemoteVolumes = db.GetRemoteVolumes();
HashSet<string> dbRemoteFiles = new HashSet<string>(dbRemoteVolumes.Select(x => x.Name));
list = list.Where(x => dbRemoteFiles.Contains(x.File.Name)).ToList();
}
}
result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Deleting);
result.OperationProgressUpdater.UpdateProgress(0);
for (var i = 0; i < list.Count; i++)
@@ -691,57 +691,55 @@ namespace Duplicati.Library.Main.Database
public void AppendFilesFromPreviousSetWithPredicate(System.Data.IDbTransaction transaction,
Func<string, long, bool> exclusionPredicate, long fileSetId, long prevFileSetId, DateTime timestamp)
{
if (exclusionPredicate == null)
{
AppendFilesFromPreviousSet(transaction, null, fileSetId, prevFileSetId, timestamp);
if (exclusionPredicate == null)
{
AppendFilesFromPreviousSet(transaction, null, fileSetId, prevFileSetId, timestamp);
return;
}
using (var cmd = m_connection.CreateCommand())
using (var cmdAdd = m_connection.CreateCommand())
using (var cmdDelete = m_connection.CreateCommand())
using (var tr = new TemporaryTransactionWrapper(m_connection, transaction))
{
var lastFilesetId =
prevFileSetId < 0 ? GetPreviousFilesetID(cmd, timestamp, fileSetId) : prevFileSetId;
long lastFilesetId = prevFileSetId < 0 ? GetPreviousFilesetID(cmd, timestamp, fileSetId) : prevFileSetId;
// prepare command for adding new entries
cmdAdd.Transaction = tr.Parent;
cmdAdd.CommandText =
@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") VALUES (?, ?, ?)";
cmdAdd.AddParameters(3);
cmdAdd.SetParameterValue(0, fileSetId);
// copy entries from previous file set into a temporary table, except those file IDs already added by the current backup
var tempFileSetTable = "FilesetEntry-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
cmd.Transaction = tr.Parent;
cmd.ExecuteNonQuery($@"CREATE TEMPORARY TABLE ""{tempFileSetTable}"" AS SELECT ""FileID"", ""Lastmodified"" FROM (SELECT DISTINCT ""FilesetID"", ""FileID"", ""Lastmodified"" FROM ""FilesetEntry"" WHERE ""FilesetID"" = ? AND ""FileID"" NOT IN (SELECT ""FileID"" FROM ""FilesetEntry"" WHERE ""FilesetID"" = ?))", lastFilesetId, fileSetId);
// enumerate files from previous set
// now we need to remove, from the above, any entries that were enumerated by the
// UNC-driven backup
cmdDelete.Transaction = tr.Parent;
cmdDelete.CommandText = $@"DELETE FROM ""{tempFileSetTable}"" WHERE ""FileID"" = ?";
cmdDelete.AddParameters(1);
// enumerate files from new temporary file set, and remove any entries handled by UNC
cmd.Transaction = tr.Parent;
foreach (var row in cmd.ExecuteReaderEnumerable(
@"SELECT
f.""Path"", fs.""FileID"", fs.""Lastmodified"", COALESCE(bs.""Length"", -1)
FROM ( SELECT DISTINCT ""FileID"", ""Lastmodified""
FROM ""FilesetEntry""
WHERE ""FilesetID"" = ?
AND ""FileID"" NOT IN (
SELECT ""FileID""
FROM ""FilesetEntry""
WHERE ""FilesetID"" = ?
)) AS fs
$@"SELECT f.""Path"", fs.""FileID"", fs.""Lastmodified"", COALESCE(bs.""Length"", -1)
FROM (SELECT DISTINCT ""FileID"", ""Lastmodified"" FROM ""{tempFileSetTable}"") AS fs
LEFT JOIN ""File"" AS f ON fs.""FileID"" = f.""ID""
LEFT JOIN ""Blockset"" AS bs ON f.""BlocksetID"" = bs.""ID"";",
lastFilesetId, fileSetId))
LEFT JOIN ""Blockset"" AS bs ON f.""BlocksetID"" = bs.""ID"";"))
{
var path = row.GetString(0);
var size = row.GetInt64(3);
if (!exclusionPredicate(path, size))
if (exclusionPredicate(path, size))
{
cmdAdd.SetParameterValue(1, row.GetInt64(1));
cmdAdd.SetParameterValue(2, row.GetInt64(2));
cmdAdd.ExecuteNonQuery();
cmdDelete.SetParameterValue(0, row.GetInt64(1));
cmdDelete.ExecuteNonQuery();
}
}
tr.Commit();
// now copy the temporary table into the FileSetEntry table
cmd.ExecuteNonQuery($@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"")
SELECT ?, ""FileID"", ""Lastmodified"" FROM ""{tempFileSetTable}""", fileSetId);
tr.Commit();
}
}
}
/// <summary>
/// Creates a timestamped backup operation to correctly associate the fileset with the time it was created.
/// </summary>
@@ -1199,7 +1199,7 @@ ORDER BY
var args = new List<object>();
foreach (var f in ((Library.Utility.FilterExpression)filter).GetSimpleList())
{
if (f.Contains('*') || f.Contains('?'))
if (type == FilterType.Wildcard)
{
sb.Append(@"""Path"" LIKE ? OR ");
args.Add(f.Replace('*', '%').Replace('?', '_'));
@@ -102,10 +102,14 @@ namespace Duplicati.Library.Main.Database
if (deleted != updated)
throw new Exception(string.Format("Unexpected number of remote volumes marked as deleted. Found {0} filesets, but {1} volumes", deleted, updated));
using (var rd = cmd.ExecuteReader(@"SELECT ""Name"", ""Size"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND ""State"" = ? ", RemoteVolumeType.Files.ToString(), RemoteVolumeState.Deleting.ToString()))
while (rd.Read())
yield return new KeyValuePair<string, long>(rd.GetString(0), rd.ConvertValueToInt64(1));
{
while (rd.Read())
{
yield return new KeyValuePair<string, long>(rd.GetString(0), rd.ConvertValueToInt64(1));
}
}
}
}
@@ -607,12 +607,12 @@ DELETE FROM ""RemoteVolume"" WHERE ""Type"" = ""{RemoteVolumeType.Blocks}"" AND
if (m_tempsmalllist != null)
try
{
cmd.CommandText = string.Format(@"DROP TABLE IF EXISTS ""{0}""", m_tempsmalllist);
cmd.ExecuteNonQuery();
}
catch { }
finally { m_tempsmalllist = null; }
{
cmd.CommandText = string.Format(@"DROP TABLE IF EXISTS ""{0}""", m_tempsmalllist);
cmd.ExecuteNonQuery();
}
catch { }
finally { m_tempsmalllist = null; }
}
@@ -1241,11 +1241,20 @@ namespace Duplicati.Library.Main.Database
public IEnumerable<IFastSource> GetFilesAndSourceBlocksFast(long blocksize)
{
var latestBlockTable = "LatestBlocksetIds-" + m_temptabsetguid;
var whereclause = string.Format(@" ""{0}"".""ID"" = ""{1}"".""FileID"" AND ""{1}"".""Restored"" = 0 AND ""{1}"".""Metadata"" = 0 AND ""{0}"".""TargetPath"" != ""{0}"".""Path"" ", m_tempfiletable, m_tempblocktable);
var sourepaths = string.Format(@"SELECT DISTINCT ""{0}"".""Path"" FROM ""{0}"", ""{1}"" WHERE " + whereclause, m_tempfiletable, m_tempblocktable);
var latestBlocksetIds = @"SELECT ""File"".""Path"", ""File"".""BlocksetID"", MAX(""Fileset"".""Timestamp"") FROM ""Fileset"", ""FilesetEntry"", ""File"" WHERE ""FilesetEntry"".""FileID"" = ""File"".""ID"" AND ""FilesetEntry"".""FilesetID"" = ""Fileset"".""ID"" AND ""File"".""Path"" IN (" + sourepaths + @") GROUP BY ""File"".""Path"" ";
var sources = string.Format(@"SELECT DISTINCT ""{0}"".""TargetPath"", ""{0}"".""Path"", ""{0}"".""ID"", ""{1}"".""Index"", ""{1}"".""Hash"", ""{1}"".""Size"" FROM ""{0}"", ""{1}"", ""File"", (" + latestBlocksetIds + @") S, ""Block"", ""BlocksetEntry"" WHERE ""BlocksetEntry"".""BlocksetID"" = ""S"".""BlocksetID"" AND ""BlocksetEntry"".""BlocksetID"" = ""File"".""BlocksetID"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""{1}"".""Index"" = ""BlocksetEntry"".""Index"" AND ""{1}"".""Hash"" = ""Block"".""Hash"" AND ""{1}"".""Size"" = ""Block"".""Size"" AND ""S"".""Path"" = ""{0}"".""Path"" AND " + whereclause + @" ORDER BY ""{0}"".""ID"", ""{1}"".""Index"" ", m_tempfiletable, m_tempblocktable);
using(var cmd = m_connection.CreateCommand())
var sourcePaths = string.Format(@"SELECT DISTINCT ""{0}"".""Path"" FROM ""{0}"", ""{1}"" WHERE " + whereclause, m_tempfiletable, m_tempblocktable);
var sources = string.Format(@"SELECT DISTINCT ""{0}"".""TargetPath"", ""{0}"".""Path"", ""{0}"".""ID"", ""{1}"".""Index"", ""{1}"".""Hash"", ""{1}"".""Size"" FROM ""{0}"", ""{1}"", ""{2}"" S, ""Block"", ""BlocksetEntry"" WHERE ""BlocksetEntry"".""BlocksetID"" = ""S"".""BlocksetID"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""{1}"".""Hash"" = ""Block"".""Hash"" AND ""{1}"".""Size"" = ""Block"".""Size"" AND ""S"".""Path"" = ""{0}"".""Path"" AND ""{1}"".""Index"" = ""BlocksetEntry"".""Index"" AND " + whereclause + @" ORDER BY ""{0}"".""ID"", ""{1}"".""Index"" ", m_tempfiletable, m_tempblocktable, latestBlockTable);
var latestBlocksetIds = @"SELECT ""File"".""Path"", ""File"".""BlocksetID"", MAX(""Fileset"".""Timestamp"") FROM ""Fileset"", ""FilesetEntry"", ""File"" WHERE ""FilesetEntry"".""FileID"" = ""File"".""ID"" AND ""FilesetEntry"".""FilesetID"" = ""Fileset"".""ID"" AND ""File"".""Path"" IN (" + sourcePaths + @") GROUP BY ""File"".""Path"" ";
using (var cmd = m_connection.CreateCommand())
{
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", latestBlockTable));
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS {1}", latestBlockTable, latestBlocksetIds));
}
using (var cmd = m_connection.CreateCommand())
using(var rd = cmd.ExecuteReader(sources))
{
if (rd.Read())
@@ -39,12 +39,12 @@ namespace Duplicati.Library.Main.Operation.Backup
{
public Task<long> LastWriteSizeAsync { get { return m_tcs.Task; } }
private readonly TaskCompletionSource<long> m_tcs = new TaskCompletionSource<long>();
public void SetFlushed(long size)
{
m_tcs.TrySetResult(size);
}
public void TrySetCanceled()
{
m_tcs.TrySetCanceled();
@@ -108,7 +108,6 @@ namespace Duplicati.Library.Main.Operation.Backup
private readonly StatsCollector m_stats;
private readonly ITaskReader m_taskReader;
public BackendUploader(Func<IBackend> backendFactory, Options options, DatabaseCommon database, ITaskReader taskReader, StatsCollector stats)
{
m_backendFactory = backendFactory;
@@ -135,6 +134,7 @@ namespace Duplicati.Library.Main.Operation.Backup
var uploadsInProgress = 0;
m_cancelTokenSource = new CancellationTokenSource();
m_progressUpdater.Run(m_cancelTokenSource.Token);
var filesetsToUpload = new List<FilesetUploadRequest>();
try
{
@@ -145,12 +145,7 @@ namespace Duplicati.Library.Main.Operation.Backup
if (!await m_taskReader.ProgressAsync)
break;
var worker = workers.FirstOrDefault(w => w.Task.IsCompleted && !w.Task.IsFaulted);
if (worker == null)
{
worker = new Worker(m_backendFactory());
workers.Add(worker);
}
Worker worker = GetWorker(workers);
if (req is VolumeUploadRequest volumeUpload)
{
@@ -164,8 +159,7 @@ namespace Duplicati.Library.Main.Operation.Backup
}
else if (req is FilesetUploadRequest filesetUpload)
{
worker.Task = Task.Run(() => UploadVolumeWriter(filesetUpload.Fileset, worker, m_cancelTokenSource.Token));
uploadsInProgress++;
filesetsToUpload.Add(filesetUpload);
}
else if (req is IndexVolumeUploadRequest indexUpload)
{
@@ -174,20 +168,10 @@ namespace Duplicati.Library.Main.Operation.Backup
}
else if (req is FlushRequest flush)
{
while (workers.Any())
{
Task finishedTask = await Task.WhenAny(workers.Select(w => w.Task)).ConfigureAwait(false);
if (finishedTask.IsFaulted)
{
flush.TrySetCanceled();
ExceptionDispatchInfo.Capture(finishedTask.Exception).Throw();
}
await WaitForWorkers(workers, flush).ConfigureAwait(false);
UploadFilesets(workers, filesetsToUpload);
await WaitForWorkers(workers, flush).ConfigureAwait(false);
Worker finishedWorker = workers.Single(w => w.Task == finishedTask);
workers.Remove(finishedWorker);
finishedWorker.Dispose();
}
flush.SetFlushed(lastSize);
uploadsInProgress = 0;
break;
@@ -240,6 +224,46 @@ namespace Duplicati.Library.Main.Operation.Backup
});
}
private Worker GetWorker(List<Worker> workers)
{
var worker = workers.FirstOrDefault(w => w.Task.IsCompleted && !w.Task.IsFaulted);
if (worker == null)
{
worker = new Worker(m_backendFactory());
workers.Add(worker);
}
return worker;
}
private async Task WaitForWorkers(List<Worker> workers, FlushRequest flush)
{
while (workers.Any())
{
Task finishedTask = await Task.WhenAny(workers.Select(w => w.Task)).ConfigureAwait(false);
if (finishedTask.IsFaulted)
{
flush.TrySetCanceled();
ExceptionDispatchInfo.Capture(finishedTask.Exception).Throw();
}
Worker finishedWorker = workers.Single(w => w.Task == finishedTask);
workers.Remove(finishedWorker);
finishedWorker.Dispose();
}
}
private void UploadFilesets(List<Worker> workers, List<FilesetUploadRequest> filesets)
{
foreach (var fileset in filesets)
{
Worker worker = GetWorker(workers);
worker.Task = Task.Run(() => UploadVolumeWriter(fileset.Fileset, worker, m_cancelTokenSource.Token));
}
filesets.Clear();
}
private static Exception GetInnerMostException(Exception ex)
{
while (ex.InnerException != null)
@@ -481,4 +505,4 @@ namespace Duplicati.Library.Main.Operation.Backup
}
}
}
}
}
@@ -20,20 +20,21 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Duplicati.Library.Utility;
using Duplicati.Library.Snapshots;
using CoCoL;
namespace Duplicati.Library.Main.Operation.Backup
{
internal static class CountFilesHandler
{
public static async Task Run(IEnumerable<string> sources, Snapshots.ISnapshotService snapshot, BackupResults result, Options options, IFilter sourcefilter, IFilter filter, Common.ITaskReader taskreader, System.Threading.CancellationToken token)
public static async Task Run(IEnumerable<string> sources, Snapshots.ISnapshotService snapshot, UsnJournalService journalService, BackupResults result, Options options, IFilter sourcefilter, IFilter filter, Common.ITaskReader taskreader, System.Threading.CancellationToken token)
{
// Make sure we create the enumeration process in a separate scope,
// but keep the log channel from the parent scope
using(Logging.Log.StartIsolatingScope(true))
using (new IsolatedChannelScope())
{
var enumeratorTask = Backup.FileEnumerationProcess.Run(sources, snapshot, null, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ExcludeEmptyFolders, options.IgnoreFilenames, options.ChangedFilelist, taskreader, token);
var enumeratorTask = Backup.FileEnumerationProcess.Run(sources, snapshot, journalService, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ExcludeEmptyFolders, options.IgnoreFilenames, options.ChangedFilelist, taskreader, token);
var counterTask = AutomationExtensions.RunTask(new
{
Input = Backup.Channels.SourcePaths.ForRead
@@ -179,8 +179,17 @@ namespace Duplicati.Library.Main.Operation.Backup
// 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 we can't find a symlink target for the current path, we won't treat it as a symlink.
string symlinkTarget = null;
try
{
symlinkTarget = snapshot.GetSymlinkTarget(path);
}
catch (Exception ex)
{
Logging.Log.WriteExplicitMessage(FILELOGTAG, "SymlinkTargetReadFailure", ex, "Failed to read symlink target for path: {0}", path);
}
if (!string.IsNullOrWhiteSpace(symlinkTarget))
{
if (options.SymlinkPolicy == Options.SymlinkStrategy.Ignore)
@@ -51,145 +51,161 @@ namespace Duplicati.Library.Main.Operation.Backup
var filehasher = HashAlgorithm.Create(options.FileHashAlgorithm);
var blockhasher = HashAlgorithm.Create(options.BlockHashAlgorithm);
var emptymetadata = Utility.WrapMetadata(new Dictionary<string, string>(), options);
var maxmetadatasize = (options.Blocksize / (long)options.BlockhashSize) * options.Blocksize;
var maxmetadatasize = (options.Blocksize / (long) options.BlockhashSize) * options.Blocksize;
if (blockhasher == null)
if (blockhasher == null)
throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported");
if (filehasher == null)
if (filehasher == null)
throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported");
if (!blockhasher.CanReuseTransform)
if (!blockhasher.CanReuseTransform)
throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported");
if (!filehasher.CanReuseTransform)
if (!filehasher.CanReuseTransform)
throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported");
using (var empty_metadata_stream = new MemoryStream(emptymetadata.Blob))
while (await taskreader.ProgressAsync)
{
var send_close = false;
var filesize = 0L;
var e = await self.Input.ReadAsync();
var cur = e.Result;
try
while (await taskreader.ProgressAsync)
{
var stream = e.Stream;
var send_close = false;
var filesize = 0L;
using (var blocklisthashes = new Library.Utility.FileBackedStringList())
using (var hashcollector = new Library.Utility.FileBackedStringList())
{
var blocklistbuffer = new byte[blocksize];
var blocklistoffset = 0L;
var e = await self.Input.ReadAsync();
var cur = e.Result;
long fslen = -1;
try { fslen = stream.Length; }
catch (Exception ex) { Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path); }
if (e.IsMetadata && fslen > maxmetadatasize)
{
//TODO: To fix this, the "WriteFileset" method in BackupHandler needs to
// be updated such that it can select sets even when there are multiple
// blocklist hashes for the metadata.
// This could be done such that an extra query is made if the metadata
// spans multiple blocklist hashes, as it is not expected to be common
Logging.Log.WriteWarningMessage(LOGTAG, "TooLargeMetadata", null, "Metadata size is {0}, but the largest accepted size is {1}, recording empty metadata for {2}", fslen, maxmetadatasize, e.Path);
empty_metadata_stream.Position = 0;
stream = empty_metadata_stream;
fslen = stream.Length;
}
// Don't send progress reports for metadata
if (!e.IsMetadata)
{
await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = fslen, Type = EventType.FileStarted });
send_close = true;
}
filehasher.Initialize();
var lastread = 0;
var buf = new byte[blocksize];
var lastupdate = DateTime.Now;
// Core processing loop, read blocks of data and hash individually
while (((lastread = await stream.ForceStreamReadAsync(buf, blocksize)) != 0))
{
// Run file hashing concurrently to squeeze a little extra concurrency out of it
var pftask = Task.Run(() => filehasher.TransformBlock(buf, 0, lastread, buf, 0));
var hashdata = blockhasher.ComputeHash(buf, 0, lastread);
var hashkey = Convert.ToBase64String(hashdata);
// If we have too many hashes, flush the blocklist
if (blocklistbuffer.Length - blocklistoffset < hashdata.Length)
{
var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int)blocklistoffset));
blocklisthashes.Add(blkey);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true);
blocklistoffset = 0;
blocklistbuffer = new byte[blocksize];
}
// Store the current hash in the blocklist
Array.Copy(hashdata, 0, blocklistbuffer, blocklistoffset, hashdata.Length);
blocklistoffset += hashdata.Length;
hashcollector.Add(hashkey);
filesize += lastread;
// Don't spam updates
if (send_close && (DateTime.Now - lastupdate).TotalSeconds > 5)
{
await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = filesize, Type = EventType.FileProgressUpdate });
lastupdate = DateTime.Now;
}
// Make sure the filehasher is done with the buf instance before we pass it on
await pftask.ConfigureAwait(false);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, hashkey, buf, 0, lastread, e.Hint, false);
buf = new byte[blocksize];
}
// If we have more than a single block of data, output the (trailing) blocklist
if (hashcollector.Count > 1)
{
var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int)blocklistoffset));
blocklisthashes.Add(blkey);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true);
}
filehasher.TransformFinalBlock(new byte[0], 0, 0);
var filehash = Convert.ToBase64String(filehasher.Hash);
var blocksetid = await database.AddBlocksetAsync(filehash, filesize, blocksize, hashcollector, blocklisthashes);
cur.SetResult(new StreamProcessResult() { Streamlength = filesize, Streamhash = filehash, Blocksetid = blocksetid });
cur = null;
}
}
catch (Exception ex)
{
try
{
if (cur != null)
cur.TrySetException(ex);
}
catch { }
var stream = e.Stream;
// Rethrow
if (ex.IsRetiredException())
throw;
}
finally
{
if (cur != null)
using (var blocklisthashes = new Library.Utility.FileBackedStringList())
using (var hashcollector = new Library.Utility.FileBackedStringList())
{
var blocklistbuffer = new byte[blocksize];
var blocklistoffset = 0L;
long fslen = -1;
try
{
fslen = stream.Length;
}
catch (Exception ex)
{
Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path);
}
if (e.IsMetadata && fslen > maxmetadatasize)
{
//TODO: To fix this, the "WriteFileset" method in BackupHandler needs to
// be updated such that it can select sets even when there are multiple
// blocklist hashes for the metadata.
// This could be done such that an extra query is made if the metadata
// spans multiple blocklist hashes, as it is not expected to be common
Logging.Log.WriteWarningMessage(LOGTAG, "TooLargeMetadata", null, "Metadata size is {0}, but the largest accepted size is {1}, recording empty metadata for {2}", fslen, maxmetadatasize, e.Path);
empty_metadata_stream.Position = 0;
stream = empty_metadata_stream;
fslen = stream.Length;
}
// Don't send progress reports for metadata
if (!e.IsMetadata)
{
await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = fslen, Type = EventType.FileStarted});
send_close = true;
}
filehasher.Initialize();
var lastread = 0;
var buf = new byte[blocksize];
var lastupdate = DateTime.Now;
// Core processing loop, read blocks of data and hash individually
while (((lastread = await stream.ForceStreamReadAsync(buf, blocksize)) != 0))
{
// Run file hashing concurrently to squeeze a little extra concurrency out of it
var pftask = Task.Run(() => filehasher.TransformBlock(buf, 0, lastread, buf, 0));
var hashdata = blockhasher.ComputeHash(buf, 0, lastread);
var hashkey = Convert.ToBase64String(hashdata);
// If we have too many hashes, flush the blocklist
if (blocklistbuffer.Length - blocklistoffset < hashdata.Length)
{
var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int) blocklistoffset));
blocklisthashes.Add(blkey);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true);
blocklistoffset = 0;
blocklistbuffer = new byte[blocksize];
}
// Store the current hash in the blocklist
Array.Copy(hashdata, 0, blocklistbuffer, blocklistoffset, hashdata.Length);
blocklistoffset += hashdata.Length;
hashcollector.Add(hashkey);
filesize += lastread;
// Don't spam updates
if (send_close && (DateTime.Now - lastupdate).TotalSeconds > 5)
{
await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = filesize, Type = EventType.FileProgressUpdate});
lastupdate = DateTime.Now;
}
// Make sure the filehasher is done with the buf instance before we pass it on
await pftask.ConfigureAwait(false);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, hashkey, buf, 0, lastread, e.Hint, false);
buf = new byte[blocksize];
}
// If we have more than a single block of data, output the (trailing) blocklist
if (hashcollector.Count > 1)
{
var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int) blocklistoffset));
blocklisthashes.Add(blkey);
await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true);
}
filehasher.TransformFinalBlock(new byte[0], 0, 0);
var filehash = Convert.ToBase64String(filehasher.Hash);
var blocksetid = await database.AddBlocksetAsync(filehash, filesize, blocksize, hashcollector, blocklisthashes);
cur.SetResult(new StreamProcessResult() {Streamlength = filesize, Streamhash = filehash, Blocksetid = blocksetid});
cur = null;
}
}
catch (Exception ex)
{
try { cur.TrySetCanceled(); }
catch { }
cur = null;
}
try
{
if (cur != null)
cur.TrySetException(ex);
}
catch
{
}
if (send_close)
await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = filesize, Type = EventType.FileClosed });
send_close = false;
// Rethrow
if (ex.IsRetiredException())
throw;
}
finally
{
if (cur != null)
{
try
{
cur.TrySetCanceled();
}
catch
{
}
cur = null;
}
if (send_close)
await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = filesize, Type = EventType.FileClosed});
send_close = false;
}
}
}
});
@@ -442,17 +442,6 @@ namespace Duplicati.Library.Main.Operation
{
try
{
// Start parallel scan, or use the database
if (m_options.DisableFileScanner)
{
var d = m_database.GetLastBackupFileCountAndSize();
m_result.OperationProgressUpdater.UpdatefileCount(d.Item1, d.Item2, true);
}
else
{
parallelScanner = Backup.CountFilesHandler.Run(sources, 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, !m_options.DisableFilelistConsistencyChecks);
@@ -505,6 +494,17 @@ namespace Duplicati.Library.Main.Operation
// create USN-based scanner if enabled
var journalService = GetJournalService(sources, snapshot, filter, lastfilesetid);
// Start parallel scan, or use the database
if (m_options.DisableFileScanner)
{
var d = m_database.GetLastBackupFileCountAndSize();
m_result.OperationProgressUpdater.UpdatefileCount(d.Item1, d.Item2, true);
}
else
{
parallelScanner = Backup.CountFilesHandler.Run(sources, snapshot, journalService, m_result, m_options, m_sourceFilter, m_filter, m_result.TaskReader, counterToken.Token);
}
// Run the backup operation
if (await m_result.TaskReader.ProgressAsync)
{
@@ -281,7 +281,6 @@ namespace Duplicati.Library.Main.Operation
/// <summary>
/// Remove backups according to the --retention-policy option.
/// Backups that are not within any of the specified time frames will will NOT be deleted.
/// Partial backups are not removed.
/// </summary>
public class RetentionPolicyRemover : FilesetRemover
@@ -85,6 +85,11 @@ namespace Duplicati.Library.Main.Operation
SetCount = db.GetFilesetFileCount(x.Item2, tr)
}).ToArray();
if (m_options.Dryrun)
tr.Rollback();
else
tr.Commit();
var fully_emptied = compare_list.Where(x => x.RemoveCount == x.SetCount).ToArray();
var to_purge = compare_list.Where(x => x.RemoveCount != x.SetCount).ToArray();
@@ -167,11 +172,6 @@ namespace Duplicati.Library.Main.Operation
}
}
if (m_options.Dryrun)
tr.Rollback();
else
tr.Commit();
m_result.OperationProgressUpdater.UpdateProgress(0.95f);
if (!m_options.Dryrun && db.RepairInProgress)
@@ -413,11 +413,27 @@ namespace Duplicati.Library.Main.Operation
throw;
}
var fileErrors = 0L;
// Restore empty files. They might not have any blocks so don't appear in any volume.
foreach (var file in database.GetFilesToRestore(true).Where(item => item.Length == 0)) {
// Just create the file and close it right away, empty statement is intentional.
using (SystemIO.IO_OS.FileCreate(file.Path))
foreach (var file in database.GetFilesToRestore(true).Where(item => item.Length == 0))
{
Logging.Log.WriteVerboseMessage(LOGTAG, "RestoreEmptyFile", "Restoring empty file \"{0}\"", file.Path);
try
{
SystemIO.IO_OS.DirectoryCreate(SystemIO.IO_OS.PathGetDirectoryName(file.Path));
// Just create the file and close it right away, empty statement is intentional.
using (SystemIO.IO_OS.FileCreate(file.Path))
{
}
}
catch (Exception ex)
{
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore empty file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex is System.Threading.ThreadAbortException)
throw;
}
}
@@ -435,8 +451,6 @@ namespace Duplicati.Library.Main.Operation
return;
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PostRestoreVerify);
var fileErrors = 0L;
if (m_options.PerformRestoredFileVerification)
{
@@ -470,7 +484,7 @@ namespace Duplicati.Library.Main.Operation
catch (Exception ex)
{
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, ex.Message);
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex is System.Threading.ThreadAbortException)
throw;
}
@@ -734,8 +748,8 @@ namespace Duplicati.Library.Main.Operation
}
}
if ((++updateCount) % 20 == 0)
blockmarker.UpdateProcessed(result.OperationProgressUpdater);
if ((++updateCount) % 20 == 0)
blockmarker.UpdateProcessed(result.OperationProgressUpdater);
}
catch (Exception ex)
{
+3 -1
View File
@@ -208,12 +208,14 @@ namespace Duplicati.Library.Main
if (string.Equals(ioclass, "idle", StringComparison.OrdinalIgnoreCase))
{
m_originalNiceClass = 3;
// Only allowed for "best-effort" and "realtime"
m_originalNiceLevel = -1;
}
else if (string.Equals(ioclass, "none", StringComparison.OrdinalIgnoreCase))
{
m_originalNiceClass = 0;
m_originalNiceLevel = int.Parse(results.Last());
// Only allowed for "best-effort" and "realtime"
m_originalNiceLevel = -1;
}
else if (string.Equals(ioclass, "best-effort", StringComparison.OrdinalIgnoreCase))
{
+1 -1
View File
@@ -10,7 +10,7 @@ namespace Duplicati.Library.Main.Strings
public static string DeprecatedOptionUsedWarning(string optionname, string message) { return LC.L(@"The option {0} is deprecated: {1}", optionname, message); }
public static string DuplicateOptionNameWarning(string optionname) { return LC.L(@"The option --{0} exists more than once, please report this to the developers", optionname); }
public static string NoSourceFoldersError { get { return LC.L(@"No source folders specified for backup"); } }
public static string SourceIsMissingError(string foldername) { return LC.L(@"The source folder {0} does not exist, aborting backup", foldername); }
public static string SourceIsMissingError(string foldername) { return LC.L(@"Backup aborted since the source path {0} does not exist. Please verify that the source path exists, or remove the source path from the backup configuration, or set the allow-missing-source option.", foldername); }
public static string SourceUnauthorizedError(string foldername) { return LC.L(@"Unauthorized to access source folder {0}, aborting backup", foldername); }
public static string UnsupportedBooleanValue(string optionname, string value) { return LC.L(@"The value ""{1}"" supplied to --{0} does not parse into a valid boolean, this will be treated as if it was set to ""true""", optionname, value); }
public static string UnsupportedEnumerationValue(string optionname, string value, string[] values) { return LC.L(@"The option --{0} does not support the value ""{1}"", supported values are: {2}", optionname, value, string.Join(", ", values)); }
@@ -135,9 +135,6 @@ namespace Duplicati.Library.Main.Volumes
if (m_streamwriter != null)
{
this.AddFilelistFile();
m_writer.Close();
m_streamwriter.Dispose();
m_streamwriter = null;
}
if (m_tempStream != null)
@@ -159,11 +156,20 @@ namespace Duplicati.Library.Main.Volumes
m_writer.Flush();
m_streamwriter.Flush();
using (Stream sr = m_compression.CreateFile(FILELIST, CompressionHint.Compressible, DateTime.UtcNow))
try
{
m_tempStream.Seek(0, SeekOrigin.Begin);
m_tempStream.CopyTo(sr);
sr.Flush();
using (Stream sr = m_compression.CreateFile(FILELIST, CompressionHint.Compressible, DateTime.UtcNow))
{
m_tempStream.Seek(0, SeekOrigin.Begin);
m_tempStream.CopyTo(sr);
sr.Flush();
}
}
finally
{
m_writer.Close();
m_streamwriter.Dispose();
m_streamwriter = null;
}
}
@@ -153,16 +153,17 @@ namespace Duplicati.Library.Modules.Builtin
public void OnStart(string operationname, ref string remoteurl, ref string[] localpath)
{
if (!string.IsNullOrEmpty(m_requiredScript))
Execute(m_requiredScript, "BEFORE", operationname, ref remoteurl, ref localpath, m_timeout, true, m_options, null, null);
if (!string.IsNullOrEmpty(m_startScript))
Execute(m_startScript, "BEFORE", operationname, ref remoteurl, ref localpath, m_timeout, false, m_options, null, null);
// Save options that might be set by a --run-script-before script so that the OnFinish method
// references the same values.
m_operationName = operationname;
m_remoteurl = remoteurl;
m_localpath = localpath;
if (!string.IsNullOrEmpty(m_requiredScript))
Execute(m_requiredScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, true, m_options, null, null);
if (!string.IsNullOrEmpty(m_startScript))
Execute(m_startScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, null, null);
}
public void OnFinish (object result)
@@ -90,8 +90,6 @@ Example with 3 recipients:
Peter Sample <peter@example.com>, John Sample <john@example.com>, admin@example.com"); } }
public static string OptionRecipientShort { get { return LC.L(@"Email recipient(s)"); } }
public static string OptionSendallLong { get { return LC.L(@"By default, mail will only be sent after a Backup operation. Use this option to send mail for all operations."); } }
public static string OptionSendallShort { get { return LC.L(@"Send email for all operations"); } }
public static string OptionSenderLong { get { return LC.L(@"Address of the email sender. If no host is supplied, the hostname of the first recipient is used. Examples of allowed formats:
sender
@@ -110,7 +108,6 @@ To enable SMTP over SSL, use the format smtps://example.com. To enable SMTP STAR
public static string OptionSubjectShort { get { return LC.L(@"The email subject"); } }
public static string OptionUsernameLong { get { return LC.L(@"The username used to authenticate with the SMTP server if required."); } }
public static string OptionUsernameShort { get { return LC.L(@"SMTP Username"); } }
public static string SendMailFailedError(string message) { return LC.L(@"Failed to send email: {0}", message); }
public static string SendMailLog(string message) { return LC.L(@"Whole SMTP communication: {0}", message); }
public static string SendMailFailedRetryError(string failedserver, string message, string retryserver) { return LC.L(@"Failed to send email with server: {0}, message: {1}, retrying with {2}", failedserver, message, retryserver); }
public static string SendMailSuccess(string server) { return LC.L(@"Email sent successfully using server: {0}", server); }
@@ -140,7 +137,6 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp
public static string DisplayName { get { return LC.L(@"XMPP report module"); } }
public static string Description { get { return LC.L(@"This module provides support for sending status reports via XMPP messages"); } }
public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while logging in to jabber server"); } }
public static string SendMessageError(string message) { return LC.L(@"Failed to send jabber message: {0}", message); }
}
internal static class SendHttpMessage {
@@ -167,9 +163,6 @@ All command line options are also reported within %value%, e.g. %volsize%. Any u
You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); }
public static string SendhttpanyoperationShort { get { return LC.L(@"Send messages for all operations"); } }
public static string SendhttpanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a Backup operation. Use this option to send messages for all operations"); } }
public static string SendMessageError(string message) { return LC.L(@"Failed to send http message: {0}", message); }
public static string SendasjsonShort { get { return LC.L(@"Send data as JSON body"); } }
public static string SendasjsonLong { get { return LC.L(@"Use this flag to send the result data as a JSON object"); } }
public static string HttpverbShort { get { return LC.L(@"Sets the HTTP verb to use"); } }
public static string HttpverbLong { get { return LC.L(@"Use this option to change the default HTTP verb used to submit a report"); } }
}
+1 -1
View File
@@ -12,4 +12,4 @@
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/></startup></configuration>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="6.2.0" targetFramework="net462" />
<package id="System.Data.SQLite" version="1.0.111.0" targetFramework="net462" />
<package id="System.Data.SQLite.Core" version="1.0.111.0" targetFramework="net462" />
<package id="System.Data.SQLite.EF6" version="1.0.111.0" targetFramework="net462" />
<package id="System.Data.SQLite.Linq" version="1.0.111.0" targetFramework="net462" />
</packages>
<package id="EntityFramework" version="6.2.0" targetFramework="net471" />
<package id="System.Data.SQLite" version="1.0.111.0" targetFramework="net471" />
<package id="System.Data.SQLite.Core" version="1.0.111.0" targetFramework="net471" />
<package id="System.Data.SQLite.EF6" version="1.0.111.0" targetFramework="net471" />
<package id="System.Data.SQLite.Linq" version="1.0.111.0" targetFramework="net471" />
</packages>
+24 -24
View File
@@ -1,5 +1,5 @@
using Duplicati.Library.Common;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Common.IO;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -45,8 +45,8 @@ namespace Duplicati.Library.Snapshots
public static bool operator ==(MSSQLDB db1, MSSQLDB db2)
{
if (object.ReferenceEquals(db1, db2)) return true;
if (object.ReferenceEquals(db1, null)) return false;
if (object.ReferenceEquals(db2, null)) return false;
if (object.ReferenceEquals(db1, null)) return false;
if (object.ReferenceEquals(db2, null)) return false;
return db1.Equals(db2);
}
@@ -58,7 +58,7 @@ namespace Duplicati.Library.Snapshots
if (object.ReferenceEquals(db2, null)) return true;
return !db1.Equals(db2);
}
}
}
public class MSSQLUtility
@@ -79,8 +79,8 @@ namespace Duplicati.Library.Snapshots
/// Enumerated MS SQL DBs
/// </summary>
public List<MSSQLDB> DBs { get { return m_DBs; } }
private readonly List<MSSQLDB> m_DBs;
private readonly List<MSSQLDB> m_DBs;
public MSSQLUtility()
{
m_DBs = new List<MSSQLDB>();
@@ -118,7 +118,7 @@ namespace Duplicati.Library.Snapshots
else if (installed32on64 != null)
try { arrInstalledInstances = (string[])installed32on64; }
catch { }
}
}
IsMSSQLInstalled = arrInstalledInstances != null && arrInstalledInstances.Length > 0;
@@ -138,23 +138,23 @@ namespace Duplicati.Library.Snapshots
m_DBs.Clear();
using (var vssBackupComponents = new VssBackupComponents())
{
var writerGUIDS = new [] { MSSQLWriterGuid };
try
{
vssBackupComponents.SetupWriters(writerGUIDS, null);
}
catch (Exception)
{
throw new Interface.UserInformationException("Microsoft SQL Server VSS Writer not found - cannot backup SQL databases.", "NoMsSqlVssWriter");
}
foreach (var o in vssBackupComponents.ParseWriterMetaData(writerGUIDS))
{
m_DBs.Add(new MSSQLDB(o.Name, o.LogicalPath + "\\" + o.Name, o.Paths.ConvertAll(m => m[0].ToString().ToUpperInvariant() + m.Substring(1))
.Distinct(Utility.Utility.ClientFilenameStringComparer)
.OrderBy(a => a).ToList()));
}
{
var writerGUIDS = new [] { MSSQLWriterGuid };
try
{
vssBackupComponents.SetupWriters(writerGUIDS, null);
}
catch (Exception)
{
throw new Interface.UserInformationException("Microsoft SQL Server VSS Writer not found - cannot backup SQL databases.", "NoMsSqlVssWriter");
}
foreach (var o in vssBackupComponents.ParseWriterMetaData(writerGUIDS))
{
m_DBs.Add(new MSSQLDB(o.Name, o.LogicalPath + "\\" + o.Name, o.Paths.ConvertAll(m => m[0].ToString().ToUpperInvariant() + m.Substring(1))
.Distinct(Utility.Utility.ClientFilenameStringComparer)
.OrderBy(a => a).ToList()));
}
}
}
}
@@ -17,6 +17,7 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Common.IO;
namespace Duplicati.Library.Snapshots
@@ -56,6 +57,18 @@ namespace Duplicati.Library.Snapshots
return SystemIO.IO_WIN.FileLength(localPath);
}
/// <summary>
/// Enumerates all files and folders in the snapshot, restricted to sources
/// </summary>
/// <param name="sources">Sources to enumerate</param>
/// <param name="callback">The callback to invoke with each found path</param>
/// <param name="errorCallback">The callback used to report errors</param>
public override IEnumerable<string> EnumerateFilesAndFolders(IEnumerable<string> sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback)
{
// For Windows, ensure we don't store paths with UNC prefix
return base.EnumerateFilesAndFolders(sources.Select(SystemIOWindows.StripUNCPrefix), callback, errorCallback);
}
/// <summary>
/// Gets the last write time of a given file in UTC
/// </summary>
@@ -138,7 +151,8 @@ namespace Duplicati.Library.Snapshots
/// <inheritdoc />
public override string ConvertToSnapshotPath(string localPath)
{
return localPath;
// For Windows, ensure we don't store paths with UNC prefix
return SystemIOWindows.StripUNCPrefix(localPath);
}
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ namespace Duplicati.Library.Snapshots
/// <param name="sources">Source paths to enumerate</param>
/// <param name="callback">The callback to invoke with each found path</param>
/// <param name="errorCallback">The callback used to report errors</param>
public IEnumerable<string> EnumerateFilesAndFolders(IEnumerable<string> sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback)
public virtual IEnumerable<string> EnumerateFilesAndFolders(IEnumerable<string> sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback)
{
return sources.SelectMany(s => Utility.Utility.EnumerateFileSystemEntries(s, callback, ListFolders, ListFiles, GetAttributes, errorCallback));
}
+135 -60
View File
@@ -37,9 +37,15 @@ namespace Duplicati.Library.Snapshots
/// </summary>
public sealed class USNJournal : IDisposable
{
/// <summary>
/// The log tag to use
/// </summary>
private static readonly string FILTER_LOGTAG = Logging.Log.LogTagFromType(typeof(USNJournal));
[Flags]
public enum ChangeReason
{
None = 0,
Modified = 1,
Created = 2,
Deleted = 4,
@@ -79,7 +85,7 @@ namespace Duplicati.Library.Snapshots
/// <summary>
/// Start USN of current cached m_entryList
/// </summary>
private long m_minUsn;
private long m_startUsn;
/// <summary>
/// The safe filehandle
@@ -133,27 +139,29 @@ namespace Duplicati.Library.Snapshots
/// Returns a list of files or folders that have changed since the recorded USN
/// </summary>
/// <param name="sourceFileOrFolder">The file or folder to find entries for</param>
/// <param name="minUsn">Minimum USN of entry</param>
/// <param name="startUsn">USN of first entry to consider</param>
/// <returns>A list of tuples with changed files and folders and their type</returns>
public IEnumerable<Tuple<string, EntryType>> GetChangedFileSystemEntries(string sourceFileOrFolder, long minUsn)
public IEnumerable<Tuple<string, EntryType>> GetChangedFileSystemEntries(string sourceFileOrFolder, long startUsn)
{
return GetChangedFileSystemEntries(sourceFileOrFolder, minUsn, ChangeReason.Any);
return GetChangedFileSystemEntries(sourceFileOrFolder, startUsn, ChangeReason.Any);
}
/// <summary>
/// Returns a list of files or folders that have changed since the recorded USN
/// </summary>
/// <param name="sourceFileOrFolder">The file or folder to find entries for</param>
/// <param name="minUsn">Minimum USN of entry</param>
/// <param name="startUsn">USN of first entry to consider</param>
/// <param name="reason">Filter expression for change reason</param>
/// <returns>A list of tuples with changed files and folders and their type</returns>
public IEnumerable<Tuple<string, EntryType>> GetChangedFileSystemEntries(string sourceFileOrFolder, long minUsn, ChangeReason reason)
public IEnumerable<Tuple<string, EntryType>> GetChangedFileSystemEntries(string sourceFileOrFolder, long startUsn, ChangeReason reason)
{
Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize", "Determine file system changes from USN for: {0}", sourceFileOrFolder);
var isFolder = sourceFileOrFolder.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal);
foreach (var r in GetRecords(minUsn))
foreach (var r in GetRecords(startUsn))
{
if (r.UsnRecord.Usn >= minUsn
if (r.UsnRecord.Usn >= startUsn
&& (reason == ChangeReason.Any || (MapChangeReason(r.UsnRecord.Reason) & reason) != 0)
&& (r.FullPath.Equals(sourceFileOrFolder, Utility.Utility.ClientFilenameStringComparison)
|| isFolder && Utility.Utility.IsPathBelowFolder(r.FullPath, sourceFileOrFolder)))
@@ -181,13 +189,19 @@ namespace Duplicati.Library.Snapshots
/// <summary>
/// Internal method to initially create and then access the cached version of the file entry list
/// <param name="startUsn">USN of first entry to consider</param>
/// </summary>
private IEnumerable<Record> GetRecords(long minUsn)
private IEnumerable<Record> GetRecords(long startUsn)
{
if (m_entryList == null || m_minUsn != minUsn)
const Win32USN.USNReason InclusionFlags = Win32USN.USNReason.USN_REASON_ANY
& ~(Win32USN.USNReason.USN_REASON_INDEXABLE_CHANGE | Win32USN.USNReason.USN_REASON_COMPRESSION_CHANGE |
Win32USN.USNReason.USN_REASON_ENCRYPTION_CHANGE | Win32USN.USNReason.USN_REASON_EA_CHANGE |
Win32USN.USNReason.USN_REASON_REPARSE_POINT_CHANGE | Win32USN.USNReason.USN_REASON_CLOSE);
if (m_entryList == null || m_startUsn != startUsn)
{
m_minUsn = minUsn;
m_entryList = ResolveFullPaths(GetRawRecords(minUsn));
m_startUsn = startUsn;
m_entryList = ResolveFullPaths(GetRawRecords(startUsn, rec => (rec.UsnRecord.Reason & InclusionFlags) != 0));
}
return m_entryList;
@@ -311,29 +325,6 @@ namespace Duplicati.Library.Snapshots
private static IEnumerable<Record> EnumerateRecords(IReadOnlyCollection<byte> entryData)
{
return new RecordEnumerator(entryData);
//if (entryData.Count <= sizeof(long))
// yield break;
//var bufferHandle = GCHandle.Alloc(entryData, GCHandleType.Pinned);
//try
//{
// var bufferPointer = bufferHandle.AddrOfPinnedObject();
// long offset = sizeof(long);
// while (offset < entryData.Count)
// {
// var entry = GetBufferedEntry(bufferPointer, offset, out var fileName);
// yield return new Record(entry, fileName);
// offset += entry.RecordLength;
// }
//}
//finally
//{
// bufferHandle.Free();
//}
}
/// <summary>
@@ -341,7 +332,7 @@ namespace Duplicati.Library.Snapshots
/// </summary>
/// <param name="startUsn">The USN number to start the list from, set to zero to get all</param>
/// <returns>A list of files and folders changed since the USN</returns>
private ICollection<Record> GetRawRecords(long startUsn)
private ICollection<Record> GetRawRecords(long startUsn, Func<Record, bool> inclusionPredicate)
{
var records = new List<Record>();
@@ -352,7 +343,6 @@ namespace Duplicati.Library.Snapshots
Win32USN.USNReason.USN_REASON_DATA_EXTEND |
Win32USN.USNReason.USN_REASON_DATA_OVERWRITE |
Win32USN.USNReason.USN_REASON_DATA_TRUNCATION |
Win32USN.USNReason.USN_REASON_EA_CHANGE |
Win32USN.USNReason.USN_REASON_FILE_CREATE |
Win32USN.USNReason.USN_REASON_FILE_DELETE |
Win32USN.USNReason.USN_REASON_HARD_LINK_CHANGE |
@@ -361,8 +351,6 @@ namespace Duplicati.Library.Snapshots
Win32USN.USNReason.USN_REASON_NAMED_DATA_TRUNCATION |
Win32USN.USNReason.USN_REASON_RENAME_NEW_NAME |
Win32USN.USNReason.USN_REASON_RENAME_OLD_NAME |
Win32USN.USNReason.USN_REASON_REPARSE_POINT_CHANGE |
Win32USN.USNReason.USN_REASON_SECURITY_CHANGE |
Win32USN.USNReason.USN_REASON_STREAM_CHANGE,
ReturnOnlyOnClose = 0,
Timeout = 0,
@@ -382,7 +370,7 @@ namespace Duplicati.Library.Snapshots
if (e == Win32USN.ERROR_INSUFFICIENT_BUFFER)
{
bufferSize = bufferSize * 2;
bufferSize *= 2;
continue;
}
@@ -392,7 +380,9 @@ namespace Duplicati.Library.Snapshots
throw new Win32Exception(e);
}
records.AddRange(EnumerateRecords(entryData).TakeWhile(rec => rec.UsnRecord.Usn >= startUsn && rec.UsnRecord.Usn < m_journal.NextUsn));
records.AddRange(EnumerateRecords(entryData)
.TakeWhile(rec => rec.UsnRecord.Usn < m_journal.NextUsn)
.Where(rec => rec.UsnRecord.Usn >= startUsn && (inclusionPredicate == null || inclusionPredicate(rec))));
readData.StartUsn = Marshal.ReadInt64(entryData, 0);
}
@@ -423,13 +413,16 @@ namespace Duplicati.Library.Snapshots
return null;
// retry, increasing buffer size
bufferSize = bufferSize * 2;
bufferSize *= 2;
}
// not really a foreach: we only check the first record
foreach (var rec in EnumerateRecords(entryData))
{
if (rec.UsnRecord.FileReferenceNumber == frn)
return rec;
break;
}
return null;
}
@@ -439,7 +432,7 @@ namespace Duplicati.Library.Snapshots
/// </summary>
/// <param name="records">The list of records with local names</param>
/// <returns>A list of USN entries with full path</returns>
private List<Record> ResolveFullPaths(ICollection<Record> records)
private IReadOnlyCollection<Record> ResolveFullPaths(ICollection<Record> records)
{
// initialize file ref-nr (FRN) to path/parent-FRN look-up table
var cache = new Dictionary<ulong, SortedRecords>();
@@ -457,10 +450,21 @@ namespace Duplicati.Library.Snapshots
}
}
// List of unresolved USN records, with FileReferenceNumber as a key
Dictionary<ulong, List<Record>> recordsByFileRefNumber = new Dictionary<ulong, List<Record>>();
// iterate through USN records
var result = new List<Record>();
var result = new Dictionary<string, Record>();
foreach (var rec in records)
{
// Add entry to list of unresolved entries, and try to resolve them at the end of the scan
if (!recordsByFileRefNumber.TryGetValue(rec.UsnRecord.FileReferenceNumber, out List<Record> fileRefHistory))
{
fileRefHistory = new List<Record>();
recordsByFileRefNumber.Add(rec.UsnRecord.FileReferenceNumber, fileRefHistory);
}
fileRefHistory.Add(rec);
var pathList = new LinkedList<Record>();
pathList.AddFirst(rec);
@@ -476,8 +480,17 @@ namespace Duplicati.Library.Snapshots
{
// parent FRN not found in look-up table, fetch it from change journal
var parentRecord = GetRecordByFileRef(parentRefNr);
parents = new SortedRecords(new List<Record> { parentRecord });
cache.Add(parentRefNr, parents);
if (parentRecord == null)
{
pathList.Clear();
break;
}
else
{
parents = new SortedRecords(new List<Record> { parentRecord });
cache.Add(parentRefNr, parents);
}
}
// take parent entry having next smaller USN
@@ -490,25 +503,87 @@ namespace Duplicati.Library.Snapshots
cur = parent;
}
// generate full path
Debug.Assert(m_volume != null, nameof(m_volume) + " != null");
var path = m_volume;
foreach (var r in pathList)
if (pathList.Count > 0)
{
path = SystemIO.IO_WIN.PathCombine(path, r.FileName);
}
// generate full path
Debug.Assert(m_volume != null, nameof(m_volume) + " != null");
var path = m_volume;
foreach (var r in pathList)
{
path = SystemIO.IO_WIN.PathCombine(path, r.FileName);
}
if (rec.UsnRecord.FileAttributes.HasFlag(Win32USN.FileAttributes.Directory))
{
path = Util.AppendDirSeparator(path);
}
if (rec.UsnRecord.FileAttributes.HasFlag(Win32USN.FileAttributes.Directory))
{
path = Util.AppendDirSeparator(path);
}
// set resolved path
rec.FullPath = path;
result.Add(rec);
// set resolved path
rec.FullPath = path;
}
}
return result;
// parse all records
foreach (var entry in recordsByFileRefNumber)
{
bool wasCreated = false;
var tempRecords = new List<Record>();
foreach (var rec in entry.Value)
{
// add entry to intermediate result set
tempRecords.Add(rec);
var reason = rec.UsnRecord.Reason;
if (reason.HasFlag(Win32USN.USNReason.USN_REASON_FILE_CREATE) || reason.HasFlag(Win32USN.USNReason.USN_REASON_RENAME_NEW_NAME))
{
wasCreated = true;
}
if (reason.HasFlag(Win32USN.USNReason.USN_REASON_FILE_DELETE) || reason.HasFlag(Win32USN.USNReason.USN_REASON_RENAME_OLD_NAME))
{
if (!wasCreated)
{
FlushRecords(tempRecords, result);
}
tempRecords.Clear();
wasCreated = false;
}
}
FlushRecords(tempRecords, result);
}
return result.Values;
}
private static void FlushRecords(List<Record> tempRecords, Dictionary<string, Record> resultRecords)
{
foreach (var rec in tempRecords)
{
if (!string.IsNullOrEmpty(rec.FullPath))
{
resultRecords[rec.FullPath] = rec;
}
// Ignore entries below \$Extend\. A clean implementation would now
// parse the MFT and look up the actual entry. But for for now, we check against
// the well-known file names.
else if (!(rec.FileName.Length == 24
&& rec.UsnRecord.FileReferenceNumber.ToString("X16") == rec.FileName.Substring(0, 16)
|| rec.FileName.Equals("$TxfLog")
|| rec.FileName.Equals("$TxfLog.blf")))
{
Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize",
"Unable to use USN due to unresolvable entry \"{0}\" with ParentFileReferenceNumber {0:X24}",
rec.FileName, rec.UsnRecord.ParentFileReferenceNumber);
throw new UsnJournalSoftFailureException(Strings.USNHelper.PathResolveError);
}
}
tempRecords.Clear();
}
private static ChangeReason MapChangeReason(Win32USN.USNReason reason)
@@ -33,6 +33,11 @@ namespace Duplicati.Library.Snapshots
{
public class UsnJournalService
{
/// <summary>
/// The log tag to use
/// </summary>
private static readonly string FILTER_LOGTAG = Logging.Log.LogTagFromType(typeof(UsnJournalService));
private readonly ISnapshotService m_snapshot;
private readonly IEnumerable<string> m_sources;
private readonly Dictionary<string, VolumeData> m_volumeDataDict;
@@ -89,6 +94,8 @@ namespace Duplicati.Library.Snapshots
// iterate over volumes
foreach (var sourcesPerVolume in SortByVolume(m_sources))
{
Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize", "Reading USN journal for volume: {0}", sourcesPerVolume.Key);
if (m_token.IsCancellationRequested) break;
var volume = sourcesPerVolume.Key;
+23 -23
View File
@@ -53,6 +53,8 @@ namespace Duplicati.Library.Snapshots
/// <param name="options">A set of commandline options</param>
public WindowsSnapshot(IEnumerable<string> sources, IDictionary<string, string> options)
{
// For Windows, ensure we don't store paths with UNC prefix
sources = sources.Select(SystemIOWindows.StripUNCPrefix);
try
{
_vssBackupComponents = new VssBackupComponents();
@@ -110,17 +112,7 @@ namespace Duplicati.Library.Snapshots
{
string[] tmp = null;
var spath = ConvertToSnapshotPath(localFolderPath);
try
{
tmp = SystemIO.IO_WIN.GetDirectories(spath);
}
catch (DirectoryNotFoundException) //TODO: Do we really need this??
{
spath = SystemIOWindows.PrefixWithUNC(spath);
tmp = SystemIO.IO_WIN.GetDirectories(spath);
}
tmp = SystemIO.IO_WIN.GetDirectories(spath);
var root = Util.AppendDirSeparator(SystemIO.IO_WIN.GetPathRoot(localFolderPath));
var volumePath = Util.AppendDirSeparator(ConvertToSnapshotPath(root));
volumePath = SystemIOWindows.PrefixWithUNC(volumePath);
@@ -145,17 +137,7 @@ namespace Duplicati.Library.Snapshots
string[] files = null;
var spath = ConvertToSnapshotPath(localFolderPath);
// First try without long UNC prefixed
try
{
files = SystemIO.IO_WIN.GetFiles(spath);
}
catch (DirectoryNotFoundException) //TODO: Do we really need this??
{
spath = SystemIOWindows.PrefixWithUNC(spath);
files = SystemIO.IO_WIN.GetFiles(spath);
}
files = SystemIO.IO_WIN.GetFiles(spath);
// convert back to non-shadow, i.e., non-vss version
var root = Util.AppendDirSeparator(SystemIO.IO_WIN.GetPathRoot(localFolderPath));
@@ -173,6 +155,18 @@ namespace Duplicati.Library.Snapshots
#region ISnapshotService Members
/// <summary>
/// Enumerates all files and folders in the snapshot, restricted to sources
/// </summary>
/// <param name="sources">Sources to enumerate</param>
/// <param name="callback">The callback to invoke with each found path</param>
/// <param name="errorCallback">The callback used to report errors</param>
public override IEnumerable<string> EnumerateFilesAndFolders(IEnumerable<string> sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback)
{
// For Windows, ensure we don't store paths with UNC prefix
return base.EnumerateFilesAndFolders(sources.Select(SystemIOWindows.StripUNCPrefix), callback, errorCallback);
}
/// <summary>
/// Gets the last write time of a given file in UTC
/// </summary>
@@ -280,13 +274,19 @@ namespace Duplicati.Library.Snapshots
/// <inheritdoc />
public override string ConvertToSnapshotPath(string localPath)
{
// For Windows, ensure we don't store paths with UNC prefix
localPath = SystemIOWindows.StripUNCPrefix(localPath);
if (!Path.IsPathRooted(localPath))
throw new InvalidOperationException();
var root = SystemIO.IO_WIN.GetPathRoot(localPath);
var volumePath = _vssBackupComponents.GetVolumeFromCache(root);
// Note: Do NOT use Path.Combine as it strips the UNC path prefix
// Note that using a simple Path.Combine() for the following code
// can result in invalid snapshot paths; e.g., if localPath is
// @"C:\", mappedPath would not have the required trailing
// directory separator.
var subPath = localPath.Substring(root.Length);
if (!subPath.StartsWith(Util.DirectorySeparatorString, StringComparison.Ordinal))
{
@@ -230,11 +230,11 @@ namespace Duplicati.Library.Utility
{
WebResponse resp = null;
try { resp = (WebResponse)r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(r); }
try { resp = r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(r) as WebResponse; }
catch {}
if (resp == null)
try { resp = (WebResponse)m_owner.m_request.GetType().GetField("webResponse", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(m_owner.m_request); }
try { resp = m_owner.m_request.GetType().GetField("webResponse", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(m_owner.m_request) as WebResponse; }
catch { }
if (resp != null)
@@ -119,6 +119,14 @@ namespace Duplicati.Library.Utility
this.Filter = filter.Substring(1, filter.Length - 2);
this.Regexp = GetFilterGroupRegex(this.Filter);
}
else if (filter.StartsWith("@", StringComparison.Ordinal))
{
// Take filter literally; i.e., don't treat wildcard
// characters as globbing characters
this.Type = FilterType.Simple;
this.Filter = filter.Substring(1);
this.Regexp = new Regex(Utility.ConvertLiteralToRegExp(this.Filter), REGEXP_OPTIONS);
}
else
{
this.Type = (filter.Contains(MULTIPLE_WILDCARD) || filter.Contains(SINGLE_WILDCARD)) ? FilterType.Wildcard : FilterType.Simple;
@@ -150,6 +158,10 @@ namespace Duplicati.Library.Utility
{
return filterString.Substring(1, filterString.Length - 2);
}
else if (filterString.StartsWith("@", StringComparison.Ordinal))
{
return Utility.ConvertLiteralToRegExp(filterString.Substring(1));
}
else
{
return Utility.ConvertGlobbingToRegExp(filterString);
@@ -284,6 +296,8 @@ namespace Duplicati.Library.Utility
return "[" + this.Filter + "]";
case FilterType.Group:
return "{" + this.Filter + "}";
case FilterType.Simple:
return "@" + this.Filter;
default:
return this.Filter;
}
@@ -20,8 +20,8 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using Duplicati.Library.Common;
using Duplicati.Library.Common;
namespace Duplicati.Library.Utility
{
public class SslCertificateValidator
@@ -65,12 +65,16 @@ namespace Duplicati.Library.Utility
try
{
certHash = Utility.ByteArrayAsHexString(cert.GetCertHash());
if (certHash != null && m_validHashes != null)
foreach(var hash in m_validHashes)
if (certHash != null && m_validHashes != null)
{
foreach (var hash in m_validHashes)
{
if (!string.IsNullOrEmpty(hash) && certHash.Equals(hash, StringComparison.OrdinalIgnoreCase))
return true;
{
return true;
}
}
}
}
catch (Exception ex)
{
+21 -2
View File
@@ -19,7 +19,6 @@ using System;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Web;
namespace Duplicati.Library.Utility
{
@@ -445,6 +444,18 @@ namespace Duplicati.Library.Utility
/// <returns>The parsed query string</returns>
/// <param name="query">The query to parse</param>
public static NameValueCollection ParseQueryString(string query)
{
return Library.Utility.Uri.ParseQueryString(query, true);
}
/// <summary>
/// Parses the query string.
/// This is a duplicate of the System.Web.HttpUtility.ParseQueryString that does not work well on Mono
/// </summary>
/// <returns>The parsed query string</returns>
/// <param name="query">The query to parse</param>
/// <param name="decodeValues">Whether to the parameter values should be decoded or not.</param>
public static NameValueCollection ParseQueryString(string query, bool decodeValues)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
@@ -455,7 +466,15 @@ namespace Duplicati.Library.Utility
var result = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
foreach (System.Text.RegularExpressions.Match m in RE_URLPARAM.Matches(query))
result.Add(UrlDecode(m.Groups["key"].Value), UrlDecode(m.Groups["value"].Success ? m.Groups["value"].Value : ""));
{
string value = m.Groups["value"].Success ? m.Groups["value"].Value : "";
if (decodeValues)
{
value = UrlDecode(value);
}
result.Add(UrlDecode(m.Groups["key"].Value), value);
}
return result;
}
+9
View File
@@ -165,6 +165,15 @@ namespace Duplicati.Library.Utility
return globexp;
}
/// <summary>
/// Convert literal path to the equivalent regular expression.
/// </summary>
public static string ConvertLiteralToRegExp(string literalPath)
{
// Escape all special characters
return Regex.Escape(literalPath);
}
/// <summary>
/// Returns a list of all files found in the given folder.
/// The search is recursive.
@@ -43,6 +43,21 @@
<Link>licenses\SQLite\License.txt</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\thirdparty\Tencentyun\download.txt">
<Link>licenses\Tencentyun\download.txt</Link>
</Content>
<Content Include="..\..\thirdparty\Tencentyun\LICENSE.txt">
<Link>licenses\Tencentyun\LICENSE.txt</Link>
</Content>
<Content Include="..\..\thirdparty\TLSharp\Homepage.txt">
<Link>licenses\TLSharp\Homepage.txt</Link>
</Content>
<Content Include="..\..\thirdparty\TLSharp\License.txt">
<Link>licenses\TLSharp\License.txt</Link>
</Content>
<Content Include="..\..\thirdparty\TLSharp\licensedata.json">
<Link>licenses\TLSharp\licensedata.json</Link>
</Content>
<Content Include="..\..\thirdparty\uplink.NET\Homepage.txt">
<Link>licenses\uplink.NET\Homepage.txt</Link>
</Content>
+13 -4
View File
@@ -18,6 +18,7 @@
using System;
using Duplicati.Server.Serialization.Interface;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace Duplicati.Server.Database
@@ -121,11 +122,19 @@ namespace Duplicati.Server.Database
public void SanitizeTargetUrl()
{
var url = new Duplicati.Library.Utility.Uri(this.TargetURL);
var filteredParameters = url.QueryParameters;
foreach (string field in UrlPasswords) {
filteredParameters.Remove(field);
NameValueCollection filteredParameters = new NameValueCollection();
if (url.Query != null)
{
// We cannot use url.QueryParameters since it contains decoded parameter values, which
// breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply
// removing password parameters, we will leave the parameters as they are in the target URL.
filteredParameters = Library.Utility.Uri.ParseQueryString(url.Query, false);
foreach (string field in this.UrlPasswords)
{
filteredParameters.Remove(field);
}
}
url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(url.QueryParameters));
url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(filteredParameters));
this.TargetURL = url.ToString();
}
+21 -17
View File
@@ -476,21 +476,25 @@ namespace Duplicati.Server.Database
internal void UpdateBackupDBPath(IBackup item, string path)
{
lock(m_lock)
using(var tr = m_connection.BeginTransaction())
using(var cmd = m_connection.CreateCommand())
lock (m_lock)
{
cmd.Transaction = tr;
cmd.Parameters.Add(cmd.CreateParameter());
((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = path;
cmd.Parameters.Add(cmd.CreateParameter());
((System.Data.IDbDataParameter)cmd.Parameters[1]).Value = item.ID;
using (var tr = m_connection.BeginTransaction())
{
using (var cmd = m_connection.CreateCommand())
{
cmd.Transaction = tr;
cmd.Parameters.Add(cmd.CreateParameter());
((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path;
cmd.Parameters.Add(cmd.CreateParameter());
((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID;
cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?";
cmd.ExecuteNonQuery();
tr.Commit();
cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?";
cmd.ExecuteNonQuery();
tr.Commit();
}
}
}
System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
Program.StatusEventNotifyer.SignalNewEvent();
}
@@ -1104,15 +1108,15 @@ namespace Duplicati.Server.Database
var prop = properties[i];
if (prop.PropertyType.IsEnum)
prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null);
prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null);
else if (prop.PropertyType == typeof(string))
prop.SetValue(item, ConvertToString(rd, i), null);
prop.SetValue(item, ConvertToString(rd, i), null);
else if (prop.PropertyType == typeof(long))
prop.SetValue(item, ConvertToInt64(rd, i), null);
prop.SetValue(item, ConvertToInt64(rd, i), null);
else if (prop.PropertyType == typeof(bool))
prop.SetValue(item, ConvertToBoolean(rd, i), null);
prop.SetValue(item, ConvertToBoolean(rd, i), null);
else if (prop.PropertyType == typeof(DateTime))
prop.SetValue(item, ConvertToDateTime(rd, i), null);
prop.SetValue(item, ConvertToDateTime(rd, i), null);
}
return item;
-2
View File
@@ -273,8 +273,6 @@ namespace Duplicati.Server
Console.WriteLine(Strings.Program.SeriousError(mex.ToString()));
return 100;
throw;
}
catch (Exception ex)
{
+25 -14
View File
@@ -795,17 +795,28 @@ namespace Duplicati.Server
if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any())
{
string message;
string titleType;
if (r.FilesWithError > 0)
{
message = $"Errors affected {r.FilesWithError} file(s).";
titleType = "Error";
}
else if (r.Errors.Any())
{
message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors.";
titleType = "Error";
}
else
{
message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings.";
titleType = "Warning";
}
Program.DataConnection.RegisterNotification(
r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error,
backup.IsTemporary ?
"Warning" : string.Format("Warning while running {0}", backup.Name),
r.FilesWithError > 0 ?
string.Format("Errors affected {0} file(s) ", r.FilesWithError) :
(r.Errors.Any() ?
string.Format("Got {0} error(s)", r.Errors.Count()) :
string.Format("Got {0} warning(s)", r.Warnings.Count())
)
,
r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error,
backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}",
message,
null,
backup.ID,
"backup:show-log",
@@ -942,11 +953,11 @@ namespace Duplicati.Server
if (filters == null || filters.Length == 0)
return null;
return
return
(from n in filters
orderby n.Order
let exp = Environment.ExpandEnvironmentVariables(n.Expression)
select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
orderby n.Order
let exp = Environment.ExpandEnvironmentVariables(n.Expression)
select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
.Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b));
}
}
+6 -2
View File
@@ -251,8 +251,12 @@ namespace Duplicati.Server
List<string> args = new List<string>();
using (System.IO.StreamReader sr = new System.IO.StreamReader(Platform.IsClientPosix ? UnixSupport.File.OpenExclusive(e.FullPath, System.IO.FileAccess.ReadWrite) : new System.IO.FileStream(e.FullPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None)))
while(!sr.EndOfStream)
args.Add(sr.ReadLine());
{
while (!sr.EndOfStream)
{
args.Add(sr.ReadLine());
}
}
commandline = args.ToArray();
+84 -80
View File
@@ -130,94 +130,98 @@ namespace Duplicati.Server.WebServer
{
var ci = ParseRequestCulture(info);
using ( Library.Localization.LocalizationService.TemporaryContext(ci))
try
using (Library.Localization.LocalizationService.TemporaryContext(ci))
{
if (ci != null)
info.Response.AddHeader("Content-Language", ci.Name);
IRESTMethod mod;
_modules.TryGetValue(module, out mod);
if (mod == null)
{
info.Response.Status = System.Net.HttpStatusCode.NotFound;
info.Response.Reason = "No such module";
}
else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get)
{
if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
{
if (info.Request.QueryString == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
}
foreach(HttpServer.HttpInputItem v in info.Request.Form)
if (!info.Request.QueryString.Contains(v.Name))
info.Request.QueryString.Add(v.Name, v.Value);
}
get.GET(key, info);
}
else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put)
put.PUT(key, info);
else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post)
{
if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] { typeof(HttpServer.HttpForm) }, null);
r.Invoke(info.Request, new object[] { new HttpServer.HttpForm(info.Request.QueryString) });
}
else
{
foreach(HttpServer.HttpInputItem v in info.Request.QueryString)
if (!info.Request.Form.Contains(v.Name))
info.Request.Form.Add(v.Name, v.Value);
}
post.POST(key, info);
}
else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete)
delete.DELETE(key, info);
else if (method == "PATCH" && mod is IRESTMethodPATCH patch)
patch.PATCH(key, info);
else
{
info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed;
info.Response.Reason = "Method is not allowed";
}
}
catch(Exception ex)
{
Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
Console.WriteLine(ex);
try
{
if (!info.Response.HeadersSent)
{
info.Response.Status = System.Net.HttpStatusCode.InternalServerError;
info.Response.Reason = "Error";
info.Response.ContentType = "text/plain";
if (ci != null)
info.Response.AddHeader("Content-Language", ci.Name);
var wex = ex;
while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex)
wex = wex.InnerException;
info.BodyWriter.WriteJsonObject(new
IRESTMethod mod;
_modules.TryGetValue(module, out mod);
if (mod == null)
{
info.Response.Status = System.Net.HttpStatusCode.NotFound;
info.Response.Reason = "No such module";
}
else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get)
{
if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
{
Message = wex.Message,
Type = wex.GetType().Name,
#if DEBUG
Stacktrace = wex.ToString()
#endif
});
info.BodyWriter.Flush();
if (info.Request.QueryString == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
}
foreach (HttpServer.HttpInputItem v in info.Request.Form)
if (!info.Request.QueryString.Contains(v.Name))
info.Request.QueryString.Add(v.Name, v.Value);
}
get.GET(key, info);
}
else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put)
put.PUT(key, info);
else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post)
{
if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null);
r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)});
}
else
{
foreach (HttpServer.HttpInputItem v in info.Request.QueryString)
if (!info.Request.Form.Contains(v.Name))
info.Request.Form.Add(v.Name, v.Value);
}
post.POST(key, info);
}
else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete)
delete.DELETE(key, info);
else if (method == "PATCH" && mod is IRESTMethodPATCH patch)
patch.PATCH(key, info);
else
{
info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed;
info.Response.Reason = "Method is not allowed";
}
}
catch (Exception flex)
catch (Exception ex)
{
Program.DataConnection.LogError("", "Reporting error gave error", flex);
Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
Console.WriteLine(ex);
try
{
if (!info.Response.HeadersSent)
{
info.Response.Status = System.Net.HttpStatusCode.InternalServerError;
info.Response.Reason = "Error";
info.Response.ContentType = "text/plain";
var wex = ex;
while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex)
wex = wex.InnerException;
info.BodyWriter.WriteJsonObject(new
{
Message = wex.Message,
Type = wex.GetType().Name,
#if DEBUG
Stacktrace = wex.ToString()
#endif
});
info.BodyWriter.Flush();
}
}
catch (Exception flex)
{
Program.DataConnection.LogError("", "Reporting error gave error", flex);
}
}
}
}
@@ -122,6 +122,12 @@ namespace Duplicati.Server.WebServer.RESTMethods
info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) });
}
public static void RemovePasswords(IBackup backup)
{
backup.SanitizeSettings();
backup.SanitizeTargetUrl();
}
private void Export(IBackup backup, RequestInfo info)
{
var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false);
@@ -129,8 +135,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false);
if (!exportPasswords)
{
backup.SanitizeSettings();
backup.SanitizeTargetUrl();
Backup.RemovePasswords(backup);
}
if (cmdline)
@@ -193,7 +193,11 @@ namespace Duplicati.Server.WebServer
var groups = GetEnvArg("SYNO_GROUP_IDS");
if (string.IsNullOrWhiteSpace(groups))
{
groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty;
groups = groups.Replace(Environment.NewLine, String.Empty);
}
if (!groups.Split(new char[] { ' ' }).Contains("101"))
{
response.Status = System.Net.HttpStatusCode.Forbidden;
@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 6.707-5.146 5.147c-.196.195-.512.195-.708 0l-1-1c-.195-.196-.195-.512 0-.708l6.5-6.5c.196-.195.512-.195.708 0l6.5 6.5c.195.196.195.512 0 .708l-1 1c-.196.195-.512.195-.708 0z" fill="#65b1dd"/></svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 6.707-5.146 5.147c-.196.195-.512.195-.708 0l-1-1c-.195-.196-.195-.512 0-.708l6.5-6.5c.196-.195.512-.195.708 0l6.5 6.5c.195.196.195.512 0 .708l-1 1c-.196.195-.512.195-.708 0z" fill="#83bb26"/></svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 6.707-5.146 5.147c-.196.195-.512.195-.708 0l-1-1c-.195-.196-.195-.512 0-.708l6.5-6.5c.196-.195.512-.195.708 0l6.5 6.5c.195.196.195.512 0 .708l-1 1c-.196.195-.512.195-.708 0z" fill="#65b1dd" transform="matrix(1 0 0 -1 0 16)"/></svg>

After

Width:  |  Height:  |  Size: 389 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 6.707-5.146 5.147c-.196.195-.512.195-.708 0l-1-1c-.195-.196-.195-.512 0-.708l6.5-6.5c.196-.195.512-.195.708 0l6.5 6.5c.195.196.195.512 0 .708l-1 1c-.196.195-.512.195-.708 0z" fill="#83bb26" transform="matrix(1 0 0 -1 0 16)"/></svg>

After

Width:  |  Height:  |  Size: 389 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><g fill="#65b1dd"><path d="m16.712 1.547c-.134-.063-.29-.063-.424 0l-16 7.5c-.176.083-.288.259-.288.453s.112.37.288.453l16 7.5c.134.063.29.063.424 0l16-7.5c.176-.083.288-.259.288-.453s-.112-.37-.288-.453z"/><g stroke="#65b1dd"><path d="m16.5 21.5 16-7.5v3l-16 7.5-16-7.5v-3z"/><path d="m16.5 21.5 16-7.5v3l-16 7.5-16-7.5v-3z" transform="translate(0 7.25)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 542 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><g fill="#65b1dd"><path d="m16.712 1.547c-.134-.063-.29-.063-.424 0l-16 7.5c-.176.083-.288.259-.288.453s.112.37.288.453l16 7.5c.134.063.29.063.424 0l16-7.5c.176-.083.288-.259.288-.453s-.112-.37-.288-.453z"/><path d="m16.5 28.198 2.288-1.073c.155-.072.336-.061.48.031.145.092.232.251.232.422v3c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031z"/><path d="m16.5 20.948 2.293-1.075c.162-.076.352-.06.498.043.147.102.228.274.212.452-.002.027-.003.055-.003.082v2.878c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031zm5.121-2.401c-.212.1-.327.333-.276.562.051.228.254.391.488.391h6.4c.074 0 .146-.016.213-.047l4.266-2c.176-.083.288-.259.288-.453v-3c0-.171-.087-.33-.232-.422-.144-.092-.325-.103-.48-.031z"/><path d="m25 23c0-.552-.448-1-1-1s-1 .448-1 1v6c0 .552.448 1 1 1s1-.448 1-1z"/><path d="m25 23c0-.552-.448-1-1-1s-1 .448-1 1v6c0 .552.448 1 1 1s1-.448 1-1z" transform="translate(4)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><g fill="#65b1dd"><path d="m16.712 1.547c-.134-.063-.29-.063-.424 0l-16 7.5c-.176.083-.288.259-.288.453s.112.37.288.453l16 7.5c.134.063.29.063.424 0l16-7.5c.176-.083.288-.259.288-.453s-.112-.37-.288-.453z"/><path d="m16.5 28.198 2.288-1.073c.155-.072.336-.061.48.031.145.092.232.251.232.422v3c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031z"/><path d="m16.5 20.948 2.293-1.075c.162-.076.352-.06.498.043.147.102.228.274.212.452-.002.027-.003.055-.003.082v2.878c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031zm5.121-2.401c-.212.1-.327.333-.276.562.051.228.254.391.488.391h6.4c.074 0 .146-.016.213-.047l4.266-2c.176-.083.288-.259.288-.453v-3c0-.171-.087-.33-.232-.422-.144-.092-.325-.103-.48-.031z"/><path d="m24.5 22.5v8l5-4z" stroke="#65b1dd"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><g fill="#65b1dd"><path d="m16.712 1.547c-.134-.063-.29-.063-.424 0l-16 7.5c-.176.083-.288.259-.288.453s.112.37.288.453l16 7.5c.134.063.29.063.424 0l16-7.5c.176-.083.288-.259.288-.453s-.112-.37-.288-.453z"/><path d="m16.5 28.198 2.288-1.073c.155-.072.336-.061.48.031.145.092.232.251.232.422v3c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031z"/><path d="m16.5 20.948 2.293-1.075c.162-.076.352-.06.498.043.147.102.228.274.212.452-.002.027-.003.055-.003.082v2.878c0 .194-.112.371-.288.453l-2.5 1.172c-.134.063-.29.063-.424 0l-16-7.5c-.176-.083-.288-.259-.288-.453v-3c0-.171.087-.33.232-.422.144-.092.325-.103.48-.031zm5.121-2.401c-.212.1-.327.333-.276.562.051.228.254.391.488.391h6.4c.074 0 .146-.016.213-.047l4.266-2c.176-.083.288-.259.288-.453v-3c0-.171-.087-.33-.232-.422-.144-.092-.325-.103-.48-.031z"/><path d="m21 10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-.1-2.73 2.71-2.73 7.08 0 9.8 2.73 2.7 7.15 2.7 9.88 0 1.36-1.35 2.04-2.92 2.04-4.9h2c0 1.98-.88 4.55-2.64 6.29-3.51 3.48-9.21 3.48-12.72 0-3.5-3.47-3.53-9.11-.02-12.58s9.14-3.47 12.65 0l2.73-2.81zm-8.5-2.12v4.25l3.5 2.08-.72 1.21-4.28-2.54v-5z" fill-rule="nonzero" transform="matrix(.666644 0 0 .666644 18.9993 19.0005)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg"><g fill="#fff"><path d="m2.5 7h4c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-4c-.276 0-.5.224-.5.5s.224.5.5.5z"/><path d="m2.5 9h2c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-2c-.276 0-.5.224-.5.5s.224.5.5.5z"/><path d="m2.5 11h2c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-2c-.276 0-.5.224-.5.5s.224.5.5.5z"/><path d="m2.5 13h3c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-3c-.276 0-.5.224-.5.5s.224.5.5.5z"/><path d="m0 14.5v-10.652c0-.356.141-.696.392-.947.016-.016.031-.031.047-.048.547-.546 1.288-.853 2.061-.853.508 0 .995-.202 1.353-.561l.94-.939c.32-.32.754-.5 1.207-.5.453 0 .887.18 1.207.5l.646.646.294.293c.358.359.845.561 1.353.561.773 0 1.514.307 2.061.853.016.017.031.032.047.048.251.251.392.591.392.947v3.652c0 .276-.224.5-.5.5s-.5-.224-.5-.5v-2.5h-10v9h10v-1.5c0-.276.224-.5.5-.5s.5.224.5.5v2c0 .276-.224.5-.5.5h-11c-.276 0-.5-.224-.5-.5zm11-10.5v-.152c0-.09-.036-.177-.099-.24-.016-.016-.031-.031-.048-.047-.358-.359-.845-.561-1.353-.561-.773 0-1.514-.307-2.061-.853-.164-.165-.292-.292-.29-.29l-.649-.65c-.133-.133-.312-.207-.5-.207-.188 0-.367.074-.5.207l-.939.94c-.547.546-1.288.853-2.061.853-.508 0-.995.202-1.353.561-.017.016-.032.031-.048.047-.063.063-.099.15-.099.24v.152z"/><path d="m9.5 11v1.5c0 .194-.112.371-.288.453s-.383.055-.532-.069l-3-2.5c-.114-.095-.18-.236-.18-.384s.066-.289.18-.384l3-2.5c.149-.124.356-.151.532-.069s.288.259.288.453v1.5h4.5v2z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#8e8e8e"><path d="m5.5.5h10v10"/><path d="m5.5 4.5h-5v11h11v-5"/><path d="m8.5 3.5h4v4"/><path d="m12.5 3.5-6 6"/></g></svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#65b1dd"><path d="m5.5.5h10v10"/><path d="m5.5 4.5h-5v11h11v-5"/><path d="m8.5 3.5h4v4"/><path d="m12.5 3.5-6 6"/></g></svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 27 26" xmlns="http://www.w3.org/2000/svg"><path d="m13.5 2c6.071 0 11 4.929 11 11s-4.929 11-11 11-11-4.929-11-11 4.929-11 11-11zm0 2c4.967 0 9 4.033 9 9s-4.033 9-9 9-9-4.033-9-9 4.033-9 9-9zm1.5 8.499c0-.827-.672-1.499-1.499-1.499h-.002c-.827 0-1.499.672-1.499 1.499v6.002c0 .827.672 1.499 1.499 1.499h.002c.827 0 1.499-.672 1.499-1.499zm-1.5-6.499c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5z" fill="#65b1dd"/></svg>

After

Width:  |  Height:  |  Size: 551 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 27 26" xmlns="http://www.w3.org/2000/svg"><path d="m12 12v-7.502c0-.827.671-1.498 1.498-1.498h.004c.827 0 1.498.671 1.498 1.498v7.502h7.5c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5h-7.5v7.502c0 .827-.671 1.498-1.498 1.498h-.004c-.827 0-1.498-.671-1.498-1.498v-7.502h-7.5c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5z" fill="#65b1dd"/></svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -0,0 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 27 26" xmlns="http://www.w3.org/2000/svg"><path d="m13.5 4 7.5 7h-1v11h-13v-11h-1zm0-3-11.5 11h3v12h17v-12h3z" fill="#65b1dd" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 257 B

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