upstream
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
@@ -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,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,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,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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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"); } }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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"); } }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -273,8 +273,6 @@ namespace Duplicati.Server
|
||||
|
||||
Console.WriteLine(Strings.Program.SeriousError(mex.ToString()));
|
||||
return 100;
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 |