Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKenneth Skovhede <kenneth@hexad.dk>2022-06-12 23:27:13 +0300
committerKenneth Skovhede <kenneth@hexad.dk>2022-06-12 23:27:13 +0300
commita88be25a12156dd8f13fca2d640e58c24799c9d5 (patch)
tree4e5950486e5eaab9c4e715e1aad131914e2b5e3a
parent63e7c4e69321aa3ee050769173e75dbd929595e4 (diff)
parent179efa04725542b32798a59f4c0fc9affb0619a1 (diff)
Merge branch 'master' of github.com:duplicati/duplicati
-rw-r--r--Duplicati CommandLine Only.sln16
-rw-r--r--Duplicati.sln6
-rw-r--r--Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj4
-rw-r--r--Duplicati/CommandLine/Duplicati.CommandLine.csproj4
-rw-r--r--Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj4
-rw-r--r--Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj4
-rw-r--r--Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs11
-rw-r--r--Duplicati/Library/Backend/File/FileBackend.cs80
-rw-r--r--Duplicati/Library/Backend/IDrive/Duplicati.Library.Backend.IDrive.csproj66
-rw-r--r--Duplicati/Library/Backend/IDrive/Duplicati.snkbin0 -> 596 bytes
-rw-r--r--Duplicati/Library/Backend/IDrive/IDriveApiClient.cs393
-rw-r--r--Duplicati/Library/Backend/IDrive/IDriveBackend.cs212
-rw-r--r--Duplicati/Library/Backend/IDrive/Properties/AssemblyInfo.cs43
-rw-r--r--Duplicati/Library/Backend/IDrive/Strings.cs18
-rw-r--r--Duplicati/Library/Backend/IDrive/app.config10
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj9
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Jottacloud.cs60
-rw-r--r--Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs70
-rw-r--r--Duplicati/Library/Backend/Jottacloud/Strings.cs9
-rw-r--r--Duplicati/Library/Backend/Jottacloud/packages.config4
-rw-r--r--Duplicati/Library/Snapshots/HyperVUtility.cs24
-rw-r--r--Duplicati/Library/Utility/Utility.cs42
-rw-r--r--Duplicati/Server/Duplicati.Server.csproj4
-rw-r--r--Duplicati/Server/WebServer/Server.cs16
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js48
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js1
-rw-r--r--Duplicati/Server/webroot/ngax/templates/backends/idrive.html13
-rw-r--r--Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html13
28 files changed, 1064 insertions, 120 deletions
diff --git a/Duplicati CommandLine Only.sln b/Duplicati CommandLine Only.sln
index 60f91ed27..3d934e79d 100644
--- a/Duplicati CommandLine Only.sln
+++ b/Duplicati CommandLine Only.sln
@@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.F
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Compression", "Duplicati\Library\Compression\Duplicati.Library.Compression.csproj", "{19ECCE09-B5EB-406C-8C57-BAC66997D469}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Encryption", "Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{94484FDB-2EFA-4CF0-9BE6-A561157B4F87}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Encryption", "Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{2CF2D90E-C25B-47AD-91E0-98451BAB8058}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Logging", "Duplicati\Library\Logging\Duplicati.Library.Logging.csproj", "{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}"
EndProject
@@ -87,6 +87,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.J
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Rclone", "Duplicati\Library\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj", "{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.IDrive", "Duplicati\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj", "{C16639F6-DACC-4DD9-86CD-8B937516B340}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -109,10 +111,10 @@ Global
{19ECCE09-B5EB-406C-8C57-BAC66997D469}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.Build.0 = Release|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.Build.0 = Release|Any CPU
{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -261,6 +263,10 @@ Global
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Duplicati.sln b/Duplicati.sln
index 5f90df87f..668a20ee0 100644
--- a/Duplicati.sln
+++ b/Duplicati.sln
@@ -107,6 +107,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{9A04CB37-DA72-4008-9703-3AC5191974E9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.IDrive", "Duplicati\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj", "{C16639F6-DACC-4DD9-86CD-8B937516B340}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -321,6 +323,10 @@ Global
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
index f344da03f..be7aa3971 100644
--- a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
+++ b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj
@@ -64,6 +64,10 @@
<Project>{F61679A9-E5DE-468A-B5A4-05F92D0143D2}</Project>
<Name>Duplicati.Library.Backend.FTP</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj">
+ <Project>{c16639f6-dacc-4dd9-86cd-8b937516b340}</Project>
+ <Name>Duplicati.Library.Backend.IDrive</Name>
+ </ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
<Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
<Name>Duplicati.Library.Backend.Jottacloud</Name>
diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj
index e6fc646ef..efd5352ea 100644
--- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj
+++ b/Duplicati/CommandLine/Duplicati.CommandLine.csproj
@@ -95,6 +95,10 @@
<Project>{FC9B7611-836F-4127-8B44-A7C31F506807}</Project>
<Name>Duplicati.Library.Backend.File</Name>
</ProjectReference>
+ <ProjectReference Include="..\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj">
+ <Project>{c16639f6-dacc-4dd9-86cd-8b937516b340}</Project>
+ <Name>Duplicati.Library.Backend.IDrive</Name>
+ </ProjectReference>
<ProjectReference Include="..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
<Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
<Name>Duplicati.Library.Backend.Jottacloud</Name>
diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
index a4d631b04..204c9690a 100644
--- a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
+++ b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj
@@ -98,6 +98,10 @@
<Project>{D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}</Project>
<Name>Duplicati.Library.Backend.HubiC</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj">
+ <Project>{c16639f6-dacc-4dd9-86cd-8b937516b340}</Project>
+ <Name>Duplicati.Library.Backend.IDrive</Name>
+ </ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
<Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
<Name>Duplicati.Library.Backend.Jottacloud</Name>
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
index df66bc6bc..542d5e9d4 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj
@@ -189,6 +189,10 @@
<Project>{F61679A9-E5DE-468A-B5A4-05F92D0143D2}</Project>
<Name>Duplicati.Library.Backend.FTP</Name>
</ProjectReference>
+ <ProjectReference Include="..\..\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj">
+ <Project>{c16639f6-dacc-4dd9-86cd-8b937516b340}</Project>
+ <Name>Duplicati.Library.Backend.IDrive</Name>
+ </ProjectReference>
<ProjectReference Include="..\..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
<Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
<Name>Duplicati.Library.Backend.Jottacloud</Name>
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
index 31b4025d9..8fe01e451 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
@@ -225,7 +225,16 @@ namespace Duplicati.GUI.TrayIcon
{
try
{
- System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
+
+ try
+ {
+ //try TLS 1.3 (type not available on .NET < 4.8)
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | (SecurityProtocolType)12288;
+ }
+ catch (NotSupportedException)
+ {
+ }
using (Connection = new HttpServerConnection(serverURL, password, saltedpassword, databaseConnection != null ? PasswordSource.Database : PasswordSource.HostedServer, disableTrayIconLogin, options))
{
diff --git a/Duplicati/Library/Backend/File/FileBackend.cs b/Duplicati/Library/Backend/File/FileBackend.cs
index 3b41484c9..4da500458 100644
--- a/Duplicati/Library/Backend/File/FileBackend.cs
+++ b/Duplicati/Library/Backend/File/FileBackend.cs
@@ -22,6 +22,7 @@ using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -56,7 +57,7 @@ namespace Duplicati.Library.Backend
{
var uri = new Utility.Uri(url);
m_path = uri.HostAndPath;
-
+
if (options.ContainsKey("auth-username"))
m_username = options["auth-username"];
if (options.ContainsKey("auth-password"))
@@ -65,7 +66,7 @@ namespace Duplicati.Library.Backend
m_username = uri.Username;
if (!string.IsNullOrEmpty(uri.Password))
m_password = uri.Password;
-
+
if (!System.IO.Path.IsPathRooted(m_path))
m_path = systemIO.PathGetFullPath(m_path);
@@ -81,7 +82,7 @@ namespace Duplicati.Library.Backend
if (!Platform.IsClientPosix)
{
System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();
-
+
for (int i = 0; i < paths.Count; i++)
{
if (paths[i].StartsWith("*:", StringComparison.Ordinal))
@@ -195,10 +196,14 @@ namespace Duplicati.Library.Backend
}
}
#else
- public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
+ public async Task PutAsync(string targetFilename, Stream sourceStream, CancellationToken cancelToken)
{
- using (System.IO.FileStream writestream = systemIO.FileCreate(GetRemoteName(remotename)))
- await Utility.Utility.CopyStreamAsync(stream, writestream, true, cancelToken, m_copybuffer);
+ string targetFilePath = GetRemoteName(targetFilename);
+ long copiedBytes = 0;
+ using (var targetStream = systemIO.FileCreate(targetFilePath))
+ copiedBytes = await Utility.Utility.CopyStreamAsync(sourceStream, targetStream, true, cancelToken, m_copybuffer);
+
+ VerifyMatchingSize(targetFilePath, sourceStream, copiedBytes);
}
#endif
@@ -209,18 +214,22 @@ namespace Duplicati.Library.Backend
Utility.Utility.CopyStream(readstream, stream, true, m_copybuffer);
}
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
+ public Task PutAsync(string targetFilename, string sourceFilePath, CancellationToken cancelToken)
{
- string path = GetRemoteName(remotename);
+ string targetFilePath = GetRemoteName(targetFilename);
if (m_moveFile)
{
- if (systemIO.FileExists(path))
- systemIO.FileDelete(path);
-
- systemIO.FileMove(filename, path);
+ if (systemIO.FileExists(targetFilePath))
+ systemIO.FileDelete(targetFilePath);
+
+ systemIO.FileMove(sourceFilePath, targetFilePath);
}
else
- systemIO.FileCopy(filename, path, true);
+ {
+ systemIO.FileCopy(sourceFilePath, targetFilePath, true);
+ }
+
+ VerifyMatchingSize(targetFilePath, sourceFilePath);
return Task.FromResult(true);
}
@@ -386,5 +395,50 @@ namespace Duplicati.Library.Backend
out ulong lpTotalNumberOfBytes,
out ulong lpTotalNumberOfFreeBytes);
}
+
+ private static void VerifyMatchingSize(string targetFilePath, string sourceFilePath)
+ {
+ try
+ {
+ var targetFileInfo = new FileInfo(targetFilePath);
+ if (!targetFileInfo.Exists)
+ throw new FileMissingException($"Target file does not exist. Target: {targetFilePath}");
+
+ var sourceFileInfo = new FileInfo(sourceFilePath);
+ if (!sourceFileInfo.Exists)
+ throw new FileMissingException($"Source file does not exist. Source: {sourceFilePath}");
+
+ if (targetFileInfo.Length != sourceFileInfo.Length)
+ throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from source file size ({sourceFileInfo.Length:n0}). Target: {targetFilePath}");
+ }
+ catch
+ {
+ try { System.IO.File.Delete(targetFilePath); } catch { }
+ throw;
+ }
+ }
+
+ private static void VerifyMatchingSize(string targetFilePath, Stream sourceStream, long? expectedLength)
+ {
+ try
+ {
+ var targetFileInfo = new FileInfo(targetFilePath);
+ if (!targetFileInfo.Exists)
+ throw new FileMissingException($"Target file does not exist. Target: {targetFilePath}");
+
+ long? sourceStreamLength = Utility.Utility.GetStreamLength(sourceStream, out bool isStreamPostion);
+
+ if (sourceStreamLength.HasValue && targetFileInfo.Length != sourceStreamLength.Value)
+ throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from the source length ({sourceStreamLength.Value:n0}){(isStreamPostion ? " - ending stream position)" : "")}. Target: {targetFilePath}");
+
+ if (expectedLength.HasValue && targetFileInfo.Length != expectedLength.Value)
+ throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from the expected length ({expectedLength.Value:n0}). Target: {targetFilePath}");
+ }
+ catch
+ {
+ try { System.IO.File.Delete(targetFilePath); } catch { }
+ throw;
+ }
+ }
}
}
diff --git a/Duplicati/Library/Backend/IDrive/Duplicati.Library.Backend.IDrive.csproj b/Duplicati/Library/Backend/IDrive/Duplicati.Library.Backend.IDrive.csproj
new file mode 100644
index 000000000..c49153699
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/Duplicati.Library.Backend.IDrive.csproj
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{C16639F6-DACC-4DD9-86CD-8B937516B340}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>Duplicati.Library.Backend.IDrive</RootNamespace>
+ <AssemblyName>Duplicati.Library.Backend.IDrive</AssemblyName>
+ <AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
+ <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
+ <UseMSBuildEngine>false</UseMSBuildEngine>
+ <TargetFrameworkProfile />
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug</OutputPath>
+ <DefineConstants>DEBUG;</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>full</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release</OutputPath>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="IDriveApiClient.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="IDriveBackend.cs" />
+ <Compile Include="Strings.cs" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <ItemGroup>
+ <ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj">
+ <Project>{C5899F45-B0FF-483C-9D38-24A9FCAAB237}</Project>
+ <Name>Duplicati.Library.Interface</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj">
+ <Project>{B68F2214-951F-4F78-8488-66E1ED3F50BF}</Project>
+ <Name>Duplicati.Library.Localization</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj">
+ <Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
+ <Name>Duplicati.Library.Utility</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Common\Duplicati.Library.Common.csproj">
+ <Project>{D63E53E4-A458-4C2F-914D-92F715F58ACF}</Project>
+ <Name>Duplicati.Library.Common</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="app.config" />
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/Duplicati/Library/Backend/IDrive/Duplicati.snk b/Duplicati/Library/Backend/IDrive/Duplicati.snk
new file mode 100644
index 000000000..e0c1e2dd8
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/Duplicati.snk
Binary files differ
diff --git a/Duplicati/Library/Backend/IDrive/IDriveApiClient.cs b/Duplicati/Library/Backend/IDrive/IDriveApiClient.cs
new file mode 100644
index 000000000..b37af6677
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/IDriveApiClient.cs
@@ -0,0 +1,393 @@
+// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using Duplicati.Library.Common.IO;
+using Duplicati.Library.Interface;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Security.Authentication;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+// Full IDrive Sync API documentation can be found here: https://www.idrivesync.com/evs/web-developers-guide.htm
+namespace Duplicati.Library.Backend.IDrive
+{
+ /// <summary>
+ /// Provides access to an IDrive Sync.
+ /// </summary>
+ public class IDriveApiClient
+ {
+ private const string IDRIVE_AUTH_CGI_URL = "https://www1.idrive.com/cgi-bin/v1/user-details.cgi";
+ private const string IDRIVE_SYNC_GET_SERVER_ADDRESS_URL = "https://evs.idrivesync.com/evs/getServerAddress";
+ private const string SUCCESS = "SUCCESS";
+ private const string MESSAGE_ATTRIBUTE = "message";
+ private const string XML_RESPONSE_TAG = "tree";
+
+ private string _idriveUsername;
+ private string _idrivePassword;
+
+ private string _syncUsername;
+ private string _syncPassword;
+ private string _syncHostname;
+
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private static SemaphoreSlim UploadSemaphore = new SemaphoreSlim(10);
+
+ public string UserAgent { get; set; } = "Duplicati-IDrive-API-Client/" + Assembly.GetExecutingAssembly().GetName().Version;
+
+ public IDriveApiClient()
+ {
+ }
+
+ public async Task LoginAsync(string username, string password)
+ {
+ _idriveUsername = username;
+ _idrivePassword = password;
+
+ await IDriveAuthAsync(_cancellationTokenSource.Token);
+ await UpdateSyncHostnameAsync(_cancellationTokenSource.Token);
+ }
+
+ private async Task IDriveAuthAsync(CancellationToken cancellationToken)
+ {
+ // IDrive auth logic was reverse engineered from code found in the IDriveForLinux PERL scripts provided by IDrive. Download from: https://www.idrive.com/linux-backup-scripts
+ // The auth response payload contains the login credentials for the associated IDrive Sync account.
+ const string methodName = IDRIVE_AUTH_CGI_URL;
+ using (var httpClient = GetHttpClient())
+ {
+ var parameters = new List<KeyValuePair<string, string>>() {
+ new KeyValuePair<string, string> ( "username", _idriveUsername),
+ new KeyValuePair<string, string> ( "password", _idrivePassword)
+ };
+ var content = new FormUrlEncodedContent(parameters);
+ using (var response = await httpClient.PostAsync(IDRIVE_AUTH_CGI_URL, content))
+ {
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
+ throw new AuthenticationException($"Failed IDrive authentication request. Server response: {response}");
+
+ // Sample response (masked): "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<root>\n <login remote_manage_ip=\"173.255.13.30\" quota=\"5000000000000\" datacenter=\"evsvirginia.idrive.com\" enctype=\"DEFAULT\" pns_sync=\"notify2.idrive.com\" remote_manage_server_https=\"wsn16s.idrive.com\" jspsrvr=\"www.idrive.com\" plan_type=\"Regular\" dedup=\"on\" username_sync=\"abc12345678901234def\" password_sync=\"fed09887654321098cba\" dedup_enabled=\"no\" desc=\"Success\" evssrvrip=\"148.51.142.138\" plan=\"Personal\" acctype=\"IBSYNC\" cnfgstat=\"SET\" accstat=\"Y\" evswebsrvr=\"evsweb5114.idrive.com\" remote_manage_websock_server=\"yes\" evssrvr=\"evs5114.idrive.com\" message=\"SUCCESS\" remote_manage_ip_https=\"173.255.13.31\" cnfgstatus=\"Y\" remote_manage_server=\"wsn16.idrive.com\" evswebsrvrip=\"148.51.142.139\" quota_used=\"100000000000\"></login>\n</root>\n"
+ string responseString = await response.Content.ReadAsStringAsync();
+ var responseXml = new XmlDocument();
+ responseXml.LoadXml(responseString);
+ var nodes = responseXml.GetElementsByTagName("login");
+ if (nodes.Count == 0)
+ throw new AuthenticationException($"Failed '{methodName}' request. Unexpected authentication response data (no login element). Server response: {response}");
+
+ var responseNode = nodes[0];
+
+ if (responseNode.Attributes[MESSAGE_ATTRIBUTE]?.Value != SUCCESS)
+ throw new AuthenticationException($"Failed IDrive authentication request. Non-{SUCCESS}. Description: {responseNode.Attributes["desc"]?.Value}");
+
+ _syncUsername = responseNode.Attributes["username_sync"]?.Value;
+ _syncPassword = responseNode.Attributes["password_sync"]?.Value;
+
+ if (string.IsNullOrEmpty(_syncUsername) || string.IsNullOrEmpty(_syncPassword))
+ throw new AuthenticationException($"Failed '{methodName}' request. IDrive Sync username and/or password were not provided. Server response: {response}");
+ }
+ }
+ }
+
+ private async Task UpdateSyncHostnameAsync(CancellationToken cancellationToken)
+ {
+ // The API docs state that the sync web API server may change over time and must be retrieved on each login.
+ // The server may be different for different accounts, depending where the data is stored.
+ var responseNode = await GetSimpleTreeResponseAsync(IDRIVE_SYNC_GET_SERVER_ADDRESS_URL, "getServerAddress", cancellationToken);
+
+ _syncHostname = responseNode.Attributes["webApiServer"]?.Value;
+
+ if (string.IsNullOrEmpty(_syncHostname))
+ throw new AuthenticationException($"Failed 'getServerAddress' request. Empty hostname. Tree XML: {responseNode.OuterXml}");
+ }
+
+ public async Task<List<FileEntry>> GetFileEntryListAsync(string directoryPath, string searchCriteria = null)
+ {
+ string methodName = string.IsNullOrEmpty(searchCriteria) ? "browseFolder" : "searchFiles";
+ // NOTE: The IDrive "searchFiles" API method has a bug that returns the name of the directory being listed as one of the items when searching for "*"
+ string url = GetSyncServiceUrl(methodName);
+ var list = new List<FileEntry>();
+
+ using (var httpClient = GetHttpClient())
+ using (var content = GetSyncPostContent(new NameValueCollection { { "p", directoryPath }, { "searchkey", searchCriteria } }))
+ using (var response = await httpClient.PostAsync(url, content))
+ {
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
+ throw new ApplicationException($"Failed '{methodName}' request. Server response: {response}");
+
+ using (var responseStream = await response.Content.ReadAsStreamAsync())
+ using (var xmlReader = XmlReader.Create(responseStream, new XmlReaderSettings() { Async = true }))
+ {
+ bool success = false;
+ while (await xmlReader.ReadAsync())
+ {
+ if (xmlReader.Name != XML_RESPONSE_TAG)
+ continue;
+
+ success = xmlReader.GetAttribute(MESSAGE_ATTRIBUTE) == SUCCESS;
+ break;
+ }
+
+ if (!success)
+ throw new ApplicationException($"Failed '{methodName}' request. Non-{SUCCESS}. Description: {xmlReader.GetAttribute("desc")}");
+
+ while (await xmlReader.ReadAsync())
+ {
+ // Item XML example: <item restype="1" resname="Myoffice.txt" size="9583" lmd="2010/05/26 01:58:57" ver="1" thumb="N"/>
+ if (xmlReader.Name != "item")
+ continue;
+
+ string resname = xmlReader.GetAttribute("resname");
+ if (string.IsNullOrEmpty(resname))
+ continue;
+
+ string restype = xmlReader.GetAttribute("restype");
+ string size = xmlReader.GetAttribute("size");
+ string lmd = xmlReader.GetAttribute("lmd");
+
+ long.TryParse(size, out long parsedSize);
+ DateTime.TryParse(lmd, out DateTime parsedModificationDate);
+
+ var fileEntry = new FileEntry(resname)
+ {
+ IsFolder = restype != "1",
+ Name = resname,
+ Size = parsedSize,
+ LastModification = parsedModificationDate
+ };
+
+ list.Add(fileEntry);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ public async Task CreateDirectoryAsync(string directoryName, string baseDirectoryPath, CancellationToken cancellationToken)
+ {
+ const string methodName = "createFolder";
+ string url = GetSyncServiceUrl(methodName);
+ try
+ {
+ await GetSimpleTreeResponseAsync(url, methodName, cancellationToken, new NameValueCollection { { "p", baseDirectoryPath }, { "foldername", directoryName } });
+ }
+ catch (Exception ex)
+ {
+ if (ex.Message.Contains(@"FOLDER ALREADY EXISTS WITH THIS NAME."))
+ return;
+
+ throw;
+ }
+ }
+
+ public async Task DeleteAsync(string filePath, CancellationToken cancellationToken, bool moveToTrash = true)
+ {
+ const string methodName = "deleteFile";
+ string url = GetSyncServiceUrl(methodName);
+ await GetSimpleTreeResponseAsync(url, methodName, cancellationToken, new NameValueCollection { { "p", filePath }, { "trash", moveToTrash ? "yes" : "no" } });
+ }
+
+ public async Task<FileEntry> UploadAsync(Stream stream, string filename, string directoryPath, CancellationToken cancellationToken)
+ {
+ const string methodName = "uploadFile";
+ string url = GetSyncServiceUrl(methodName);
+
+ await UploadSemaphore.WaitAsync(cancellationToken);
+
+ try
+ {
+ if (cancellationToken.IsCancellationRequested)
+ throw new OperationCanceledException();
+
+ long? streamLength = null;
+ try { streamLength = stream.Length; }
+ catch { } // Fail gracefully is stream does not support Length
+
+ using (var httpClient = GetHttpClient(TimeSpan.FromHours(24)))
+ using (var content = GetSyncPostContent(new NameValueCollection { { "p", directoryPath } }, isMultiPart: true))
+ using (var streamContent = new StreamContent(stream))
+ {
+ ((MultipartFormDataContent)content).Add(streamContent, filename, filename);
+
+ using (var response = await httpClient.PostAsync(url, content, cancellationToken))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ throw new OperationCanceledException();
+
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
+ throw new ApplicationException($"Failed '{methodName}' request. Server response: {response}");
+
+ string responseString = await response.Content.ReadAsStringAsync();
+ var responseXml = new XmlDocument();
+ responseXml.LoadXml(responseString);
+ var nodes = responseXml.GetElementsByTagName(XML_RESPONSE_TAG);
+ if (nodes.Count == 0)
+ throw new ApplicationException($"Failed '{methodName}' request. Unexpected response. Server response: {response}");
+
+ var responseNode = nodes[0];
+ if (responseNode == null)
+ throw new ApplicationException($"Failed '{methodName}' request. Missing {XML_RESPONSE_TAG} node. Server response: {response}");
+
+ if (responseNode.Attributes[MESSAGE_ATTRIBUTE]?.Value != SUCCESS)
+ throw new ApplicationException($"Failed '{methodName}' request. Non-{SUCCESS}. Description: {responseNode.Attributes["desc"]?.Value}");
+ }
+ }
+
+ // Double check the upload by searching for the file on the server and validating the response
+ var fileEntryList = await GetFileEntryListAsync(directoryPath, filename);
+ if (fileEntryList.Count != 1)
+ throw new FileMissingException($"Upload failed. File not found on server.");
+
+ var fileEntry = fileEntryList[0];
+
+ if (streamLength != null && fileEntry.Size != streamLength.Value)
+ throw new FileMissingException($"Upload failed. File size on server does not match source.");
+
+ if (fileEntry.Name != filename)
+ throw new FileMissingException($"Upload failed. Wrong file not found on server."); // this should never happen
+
+ return fileEntry;
+ }
+ finally
+ {
+ UploadSemaphore.Release();
+ }
+ }
+
+ public async Task DownloadAsync(string filePath, Stream stream)
+ {
+ const string methodName = "downloadFile";
+ string url = GetSyncServiceUrl(methodName);
+
+ using (var httpClient = GetHttpClient())
+ using (var content = GetSyncPostContent(new NameValueCollection { { "p", filePath } }))
+ using (var response = await httpClient.PostAsync(url, content))
+ {
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
+ throw new FileMissingException($"Failed '{methodName}' request. Server response: {response}");
+
+ response.Headers.TryGetValues("RESTORE_STATUS", out var restoreStatus); // The download API uses RESTORE_STATUS to indicate success instead of body XML
+
+ using (var responseStream = await response.Content.ReadAsStreamAsync())
+ {
+ if (restoreStatus.FirstOrDefault() == SUCCESS)
+ {
+ Library.Utility.Utility.CopyStream(responseStream, stream);
+ return;
+ }
+
+ using (var xmlReader = XmlReader.Create(responseStream, new XmlReaderSettings() { Async = true }))
+ {
+ bool success = false;
+ while (await xmlReader.ReadAsync())
+ {
+ if (xmlReader.Name != XML_RESPONSE_TAG)
+ continue;
+
+ success = xmlReader.GetAttribute(MESSAGE_ATTRIBUTE) == SUCCESS;
+ break;
+ }
+
+ if (!success)
+ throw new FileMissingException($"Failed '{methodName}' request. Non-{SUCCESS}. Description: {xmlReader.GetAttribute("desc")}");
+
+ throw new FileMissingException($"Failed '{methodName}' request. Invalid RESTORE_STATUS result with invalid {SUCCESS} message."); // this should never happen
+ }
+ }
+ }
+ }
+
+ private HttpClient GetHttpClient(TimeSpan? timeout = null)
+ {
+ var httpClient = new HttpClient()
+ {
+ Timeout = timeout ?? TimeSpan.FromMinutes(5) // more generous default timeout
+ };
+
+ if (!string.IsNullOrEmpty(UserAgent))
+ httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
+
+ return httpClient;
+ }
+
+ private string GetSyncServiceUrl(string serviceName)
+ {
+ return $"https://{_syncHostname}/evs/{serviceName}";
+ }
+
+ private async Task<XmlNode> GetSimpleTreeResponseAsync(string url, string methodName, CancellationToken cancellationToken, NameValueCollection parameters = null)
+ {
+ using (var httpClient = GetHttpClient())
+ using (var content = GetSyncPostContent(parameters))
+ using (var response = await httpClient.PostAsync(url, content, cancellationToken))
+ {
+ if (response.StatusCode != System.Net.HttpStatusCode.OK)
+ throw new ApplicationException($"Failed '{methodName}' request. Server response: {response}");
+
+ // Sample response: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tree message=\"SUCCESS\" cmdUtilityServer=\"evs19.idrivesync.com\"\n cmdUtilityServerIP=\"4.71.135.136\"\n webApiServer=\"evsweb19.idrivesync.com\" webApiServerIP=\"4.71.135.137\"\n faceWebApiServer=\"\" faceWebApiServerIP=\"\" dedup=\"off\"/>\n"
+ string responseString = await response.Content.ReadAsStringAsync();
+ var responseXml = new XmlDocument();
+ responseXml.LoadXml(responseString);
+ var nodes = responseXml.GetElementsByTagName(XML_RESPONSE_TAG);
+ if (nodes.Count == 0)
+ throw new ApplicationException($"Failed '{methodName}' request. Unexpected response. Server response: {response}");
+
+ var responseNode = nodes[0];
+ if (responseNode == null)
+ throw new ApplicationException($"Failed '{methodName}' request. Missing {XML_RESPONSE_TAG} node. Server response: {response}");
+
+ if (responseNode.Attributes[MESSAGE_ATTRIBUTE]?.Value != SUCCESS)
+ throw new ApplicationException($"Failed '{methodName}' request. Non-{SUCCESS}. Description: {responseNode.Attributes["desc"]?.Value}");
+
+ return responseNode;
+ }
+ }
+
+ private HttpContent GetSyncPostContent(NameValueCollection parameters = null, bool isMultiPart = false)
+ {
+ var allParameters = new List<KeyValuePair<string, string>>()
+ {
+ new KeyValuePair<string, string> ( "uid", _syncUsername),
+ new KeyValuePair<string, string> ( "pwd", _syncPassword)
+ };
+
+ if (parameters != null)
+ {
+ foreach (string key in parameters.Keys)
+ {
+ allParameters.Add(new KeyValuePair<string, string>(key, parameters[key]));
+ }
+ }
+
+ if (!isMultiPart)
+ return new FormUrlEncodedContent(allParameters);
+
+ var content = new MultipartFormDataContent(Guid.NewGuid().ToString());
+ foreach (var parameter in allParameters)
+ {
+ content.Add(new StringContent(parameter.Value, Encoding.UTF8), parameter.Key);
+ }
+
+ return content;
+ }
+ }
+}
diff --git a/Duplicati/Library/Backend/IDrive/IDriveBackend.cs b/Duplicati/Library/Backend/IDrive/IDriveBackend.cs
new file mode 100644
index 000000000..ba8561289
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/IDriveBackend.cs
@@ -0,0 +1,212 @@
+// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using Duplicati.Library.Common.IO;
+using Duplicati.Library.Interface;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Duplicati.Library.Backend.IDrive
+{
+ // ReSharper disable once UnusedMember.Global
+ // This class is instantiated dynamically in the BackendLoader.
+ public class IDriveBackend : IBackend, IStreamingBackend
+ {
+ private readonly string _username = null;
+ private readonly string _password = null;
+ private readonly string _baseDirectoryPath = null;
+ public string DisplayName => Strings.IDriveBackend.DisplayName;
+ public string Description => Strings.IDriveBackend.Description;
+ public string[] DNSName => null;
+ public string ProtocolKey => "idrive";
+ CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ public IList<ICommandLineArgument> SupportedCommands
+ {
+ get
+ {
+ return new List<ICommandLineArgument>(new ICommandLineArgument[] {
+ new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.IDriveBackend.AuthPasswordDescriptionShort, Strings.IDriveBackend.AuthPasswordDescriptionLong),
+ new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.IDriveBackend.AuthUsernameDescriptionShort, Strings.IDriveBackend.AuthUsernameDescriptionLong)
+ });
+ }
+ }
+
+ private Dictionary<string, FileEntry> _fileCache;
+ protected Dictionary<string, FileEntry> FileCache
+ {
+ get
+ {
+ if (_fileCache == null)
+ ResetFileCacheAsync().Wait();
+
+ return _fileCache;
+ }
+ set
+ {
+ _fileCache = value;
+ }
+ }
+
+ private IDriveApiClient _client;
+ protected IDriveApiClient Client
+ {
+ get
+ {
+ if (_client == null)
+ {
+ var cl = new IDriveApiClient();
+ cl.LoginAsync(_username, _password).Wait();
+ _client = cl;
+ }
+
+ return _client;
+ }
+ }
+
+ public IDriveBackend()
+ {
+ }
+
+ public IDriveBackend(string url, Dictionary<string, string> options)
+ {
+ var uri = new Utility.Uri(url); // Sample url: idrive://Directory1/SubDirectory1?auth-username=MyUsername&auth-password=MyPassword
+ _username = uri.Username;
+ _password = uri.Password;
+
+ if (string.IsNullOrEmpty(_username) && options.TryGetValue("auth-username", out string username))
+ _username = username;
+ if (string.IsNullOrEmpty(_password) && options.TryGetValue("auth-password", out string password))
+ _password = password;
+
+ if (string.IsNullOrEmpty(_username))
+ throw new UserInformationException(Strings.IDriveBackend.NoUsernameError, "IDriveNoUsername");
+ if (string.IsNullOrEmpty(_password))
+ throw new UserInformationException(Strings.IDriveBackend.NoPasswordError, "IDriveNoPassword");
+
+ _baseDirectoryPath = ("/" + (uri.HostAndPath ?? "").Trim('/') + "/").Replace("//", "/");
+ }
+
+ private async Task ResetFileCacheAsync()
+ {
+ FileCache = (await Client.GetFileEntryListAsync(_baseDirectoryPath))
+ .Where(x => !x.IsFolder)
+ .ToDictionary(x => x.Name, x => x);
+ }
+
+ public IEnumerable<IFileEntry> List()
+ {
+ return FileCache.Values;
+ }
+
+ public void Get(string filename, string localFilePath)
+ {
+ using (var fileStream = File.Create(localFilePath))
+ Get(filename, fileStream);
+ }
+
+ public void Get(string filename, Stream stream)
+ {
+ Client.DownloadAsync(Path.Combine(_baseDirectoryPath, filename), stream).Wait();
+ }
+
+ public async Task PutAsync(string filename, string localFilePath, CancellationToken cancellationToken)
+ {
+ using (var fileStream = File.OpenRead(localFilePath))
+ await PutAsync(filename, fileStream, cancellationToken);
+ }
+
+ public async Task PutAsync(string filename, Stream stream, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var fileEntry = await Client.UploadAsync(stream, filename, _baseDirectoryPath, cancellationToken);
+ FileCache[filename] = fileEntry;
+ }
+ catch
+ {
+ FileCache = null;
+ throw;
+ }
+ }
+
+ public void Delete(string filename)
+ {
+ try
+ {
+ if (!FileCache.ContainsKey(filename))
+ {
+ ResetFileCacheAsync().Wait();
+
+ if (!FileCache.ContainsKey(filename))
+ throw new FileMissingException();
+ }
+
+ Client.DeleteAsync(Path.Combine(_baseDirectoryPath, filename), _cancellationTokenSource.Token, false).Wait();
+
+ FileCache.Remove(filename);
+ }
+ catch
+ {
+ FileCache = null;
+ throw;
+ }
+ }
+
+ public void CreateFolder()
+ {
+ var directoryParts = _baseDirectoryPath.Split('/').Where(d => !string.IsNullOrEmpty(d));
+ string baseDirectory = "/";
+
+ foreach (string directoryPart in directoryParts)
+ {
+ Client.CreateDirectoryAsync(directoryPart, baseDirectory, _cancellationTokenSource.Token).Wait();
+ baseDirectory += directoryPart + "/";
+ }
+ }
+
+ public void Test()
+ {
+ this.TestList();
+ }
+
+ #region IDisposable Support
+ private bool _disposedValue = false; // To detect redundant calls
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);
+ }
+ #endregion
+ }
+}
diff --git a/Duplicati/Library/Backend/IDrive/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/IDrive/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..80c373101
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/Properties/AssemblyInfo.cs
@@ -0,0 +1,43 @@
+// Copyright (C) 2022, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("Duplicati.Library.Backend.IDrive")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("Doug Krahmer")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("2.0.0.1")]
+
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
+
diff --git a/Duplicati/Library/Backend/IDrive/Strings.cs b/Duplicati/Library/Backend/IDrive/Strings.cs
new file mode 100644
index 000000000..9bb775e29
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/Strings.cs
@@ -0,0 +1,18 @@
+using Duplicati.Library.Localization.Short;
+
+namespace Duplicati.Library.Backend.Strings
+{
+ internal static class IDriveBackend
+ {
+ public static string DisplayName { get { return LC.L(@"IDrive"); } }
+ public static string AuthPasswordDescriptionLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
+ public static string AuthPasswordDescriptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
+ public static string AuthUsernameDescriptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
+ public static string AuthUsernameDescriptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
+ public static string AuthTwoFactorKeyDescriptionShort { get { return LC.L(@"The shared secret used to generate two-factor TOTP codes."); } }
+ public static string AuthTwoFactorKeyDescriptionLong { get { return LC.L(@"For accounts with two-factor authentication enabled, this is the shared secret used to generate the two-factor TOTP codes."); } }
+ public static string NoPasswordError { get { return LC.L(@"No password given"); } }
+ public static string NoUsernameError { get { return LC.L(@"No username given"); } }
+ public static string Description { get { return LC.L(@"This backend can read and write data to IDrive Sync. Allowed formats are: ""idrive://folder/subfolder"""); } }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/IDrive/app.config b/Duplicati/Library/Backend/IDrive/app.config
new file mode 100644
index 000000000..ad53561b2
--- /dev/null
+++ b/Duplicati/Library/Backend/IDrive/app.config
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
+ </startup>
+ <runtime>
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+ </assemblyBinding>
+ </runtime>
+</configuration>
diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
index 9cdfd56bf..07d34fd01 100644
--- a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
+++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj
@@ -36,11 +36,15 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <HintPath>..\..\..\..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+ </Reference>
<Reference Include="System" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Jottacloud.cs" />
+ <Compile Include="JottacloudAuthHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Strings.cs" />
</ItemGroup>
@@ -65,9 +69,14 @@
<Project>{D63E53E4-A458-4C2F-914D-92F715F58ACF}</Project>
<Name>Duplicati.Library.Common</Name>
</ProjectReference>
+ <ProjectReference Include="..\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
+ <Project>{d4c37c33-5e73-4b56-b2c3-dc4a6baa36bb}</Project>
+ <Name>Duplicati.Library.OAuthHelper</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="Duplicati.snk" />
+ <None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
index 9f297abf2..2ebfcc7c1 100644
--- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
+++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs
@@ -31,9 +31,9 @@ namespace Duplicati.Library.Backend
// This class is instantiated dynamically in the BackendLoader.
public class Jottacloud : IBackend, IStreamingBackend
{
+ private const string AUTHID_OPTION = "authid";
private const string JFS_ROOT = "https://jfs.jottacloud.com/jfs";
private const string JFS_ROOT_UPLOAD = "https://up.jottacloud.com/jfs"; // Separate host for uploading files
- private const string API_VERSION = "2.4"; // Hard coded per 09. March 2017.
private const string JFS_BUILTIN_DEVICE = "Jotta"; // The built-in device used for the built-in Sync and Archive mount points.
private static readonly string JFS_DEFAULT_BUILTIN_MOUNT_POINT = "Archive"; // When using the built-in device we pick this mount point as our default.
private static readonly string JFS_DEFAULT_CUSTOM_MOUNT_POINT = "Duplicati"; // When custom device is specified then we pick this mount point as our default.
@@ -51,7 +51,6 @@ namespace Duplicati.Library.Backend
private readonly string m_url_device;
private readonly string m_url;
private readonly string m_url_upload;
- private readonly System.Net.NetworkCredential m_userInfo;
private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE];
private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb";
@@ -59,6 +58,8 @@ namespace Duplicati.Library.Backend
private readonly int m_threads;
private readonly long m_chunksize;
+ private readonly JottacloudAuthHelper m_oauth;
+
/// <summary>
/// The default maximum number of concurrent connections allowed by a ServicePoint object is 2.
/// It should be increased to allow multiple download threads.
@@ -132,40 +133,21 @@ namespace Duplicati.Library.Backend
m_mountPoint = JFS_DEFAULT_CUSTOM_MOUNT_POINT; // Set a suitable default mount point for custom (backup) devices.
}
+ string authid = null;
+ if (options.ContainsKey(AUTHID_OPTION))
+ authid = options[AUTHID_OPTION];
+ m_oauth = new JottacloudAuthHelper(authid);
+
// Build URL
var u = new Utility.Uri(url);
m_path = u.HostAndPath; // Host and path of "jottacloud://folder/subfolder" is "folder/subfolder", so the actual folder path within the mount point.
if (string.IsNullOrEmpty(m_path)) // Require a folder. Actually it is possible to store files directly on the root level of the mount point, but that does not seem to be a good option.
throw new UserInformationException(Strings.Jottacloud.NoPathError, "JottaNoPath");
m_path = Util.AppendDirSeparator(m_path, "/");
- if (!string.IsNullOrEmpty(u.Username))
- {
- m_userInfo = new System.Net.NetworkCredential();
- m_userInfo.UserName = u.Username;
- if (!string.IsNullOrEmpty(u.Password))
- m_userInfo.Password = u.Password;
- else if (options.ContainsKey("auth-password"))
- m_userInfo.Password = options["auth-password"];
- }
- else
- {
- if (options.ContainsKey("auth-username"))
- {
- m_userInfo = new System.Net.NetworkCredential();
- m_userInfo.UserName = options["auth-username"];
- if (options.ContainsKey("auth-password"))
- m_userInfo.Password = options["auth-password"];
- }
- }
- if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.UserName))
- throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername");
- if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.Password))
- throw new UserInformationException(Strings.Jottacloud.NoPasswordError, "JottaNoPassword");
- if (m_userInfo != null) // Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code
- m_userInfo.Domain = "";
- m_url_device = JFS_ROOT + "/" + m_userInfo.UserName + "/" + m_device;
+
+ m_url_device = JFS_ROOT + "/" + m_oauth.Username + "/" + m_device;
m_url = m_url_device + "/" + m_mountPoint + "/" + m_path;
- m_url_upload = JFS_ROOT_UPLOAD + "/" + m_userInfo.UserName + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url.
+ m_url_upload = JFS_ROOT_UPLOAD + "/" + m_oauth.Username + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url.
m_threads = int.Parse(options.ContainsKey(JFS_THREADS) ? options[JFS_THREADS] : JFS_DEFAULT_THREADS);
@@ -186,7 +168,7 @@ namespace Duplicati.Library.Backend
m_chunksize = chunksize;
}
- #region IBackend Members
+#region IBackend Members
public string DisplayName
{
@@ -329,8 +311,7 @@ namespace Duplicati.Library.Backend
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
- new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.DescriptionAuthPasswordShort, Strings.Jottacloud.DescriptionAuthPasswordLong),
- new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionAuthUsernameShort, Strings.Jottacloud.DescriptionAuthUsernameLong),
+ new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.AuthidShort, Strings.Jottacloud.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("jottacloud"))),
new CommandLineArgument(JFS_DEVICE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionDeviceShort, Strings.Jottacloud.DescriptionDeviceLong(JFS_MOUNT_POINT_OPTION)),
new CommandLineArgument(JFS_MOUNT_POINT_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionMountPointShort, Strings.Jottacloud.DescriptionMountPointLong(JFS_DEVICE_OPTION)),
new CommandLineArgument(JFS_THREADS, CommandLineArgument.ArgumentType.Integer, Strings.Jottacloud.ThreadsShort, Strings.Jottacloud.ThreadsLong, JFS_DEFAULT_THREADS),
@@ -368,26 +349,19 @@ namespace Duplicati.Library.Backend
}
}
- #endregion
+#endregion
- #region IDisposable Members
+#region IDisposable Members
public void Dispose()
{
}
- #endregion
+#endregion
private System.Net.HttpWebRequest CreateRequest(string method, string url, string queryparams)
{
- System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams));
- req.Method = method;
- req.Credentials = m_userInfo;
- req.PreAuthenticate = true; // We need this under Mono for some reason, and it appears some servers require this as well
- req.KeepAlive = false;
- req.UserAgent = "Duplicati Jottacloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
- req.Headers.Add("x-jftp-version", API_VERSION);
- return req;
+ return m_oauth.CreateRequest(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams), method);
}
private System.Net.HttpWebRequest CreateRequest(string method, string remotename, string queryparams, bool upload)
diff --git a/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs
new file mode 100644
index 000000000..d8a5d2f48
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs
@@ -0,0 +1,70 @@
+// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using Duplicati.Library.Interface;
+using Newtonsoft.Json;
+
+namespace Duplicati.Library.Backend
+{
+ public class JottacloudAuthHelper : OAuthHelper
+ {
+ private const string USERINFO_URL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/userinfo";
+ private string m_username;
+
+ public JottacloudAuthHelper(string accessToken)
+ : base(accessToken, "jottacloud")
+ {
+ base.AutoAuthHeader = true;
+
+ var userinfo = GetJSONData<UserInfo>(USERINFO_URL);
+ if (userinfo == null || string.IsNullOrEmpty(userinfo.Username))
+ throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername");
+ m_username = userinfo.Username;
+ }
+
+ public string Username
+ {
+ get
+ {
+ return m_username;
+ }
+ }
+
+ private class UserInfo
+ {
+ [JsonProperty("sub")]
+ public string Subject { get; set; }
+ [JsonProperty("email_verified")]
+ public bool EmailVerified { get; set; }
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ [JsonProperty("realm")]
+ public string Realm { get; set; }
+ [JsonProperty("preferred_username")]
+ public string PreferredUsername { get; set; } // The numeric internal username, same as Username
+ [JsonProperty("given_name")]
+ public string GivenName { get; set; }
+ [JsonProperty("family_name")]
+ public string FamilyName { get; set; }
+ [JsonProperty("email")]
+ public string Email { get; set; }
+ [JsonProperty("username")]
+ public string Username { get; set; } // The numeric internal username, same as PreferredUsername
+ }
+ }
+
+}
+
diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs
index 08ba011b8..010050022 100644
--- a/Duplicati/Library/Backend/Jottacloud/Strings.cs
+++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs
@@ -4,15 +4,12 @@ namespace Duplicati.Library.Backend.Strings {
{
public static string DisplayName { get { return LC.L(@"Jottacloud"); } }
public static string Description { get { return LC.L(@"This backend can read and write data to Jottacloud using it's REST protocol. Allowed format is ""jottacloud://folder/subfolder""."); } }
- public static string NoUsernameError { get { return LC.L(@"No username given"); } }
- public static string NoPasswordError { get { return LC.L(@"No password given"); } }
+ public static string AuthidShort { get { return LC.L(@"The authorization code"); } }
+ public static string AuthidLong(string url) { return LC.L(@"The authorization token retrieved from {0}", url); }
+ public static string NoUsernameError { get { return LC.L(@"No username found"); } }
public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } }
public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } }
public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } }
- public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } }
- public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } }
- public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
- public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } }
public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } }
public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); }
public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } }
diff --git a/Duplicati/Library/Backend/Jottacloud/packages.config b/Duplicati/Library/Backend/Jottacloud/packages.config
new file mode 100644
index 000000000..a75532f5f
--- /dev/null
+++ b/Duplicati/Library/Backend/Jottacloud/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
+</packages> \ No newline at end of file
diff --git a/Duplicati/Library/Snapshots/HyperVUtility.cs b/Duplicati/Library/Snapshots/HyperVUtility.cs
index 338706c85..423b24911 100644
--- a/Duplicati/Library/Snapshots/HyperVUtility.cs
+++ b/Duplicati/Library/Snapshots/HyperVUtility.cs
@@ -302,7 +302,16 @@ namespace Duplicati.Library.Snapshots
select ((string[])systemBaseObj["Connection"])[0]).ToList();
foreach (var vhd in tempvhd)
- result.Add(vhd);
+ {
+ if (File.Exists(vhd))
+ {
+ result.Add(vhd);
+ }
+ else
+ {
+ Logging.Log.WriteWarningMessage(LOGTAG, "HyperVInvalidVhd", null, "Invalid VHD file detected, file does not exist: {0}", vhd);
+ }
+ }
}
}
@@ -319,11 +328,16 @@ namespace Duplicati.Library.Snapshots
if (outParams != null)
{
var doc = new System.Xml.XmlDocument();
- doc.LoadXml((string)outParams[_wmiv2Namespace ? "SettingData" : "Info"]);
- var node = doc.SelectSingleNode("//PROPERTY[@NAME = 'ParentPath']/VALUE/child::text()");
+ var propertyValue = (string)outParams[_wmiv2Namespace ? "SettingData" : "Info"];
+
+ if (propertyValue != null)
+ {
+ doc.LoadXml(propertyValue);
+ var node = doc.SelectSingleNode("//PROPERTY[@NAME = 'ParentPath']/VALUE/child::text()");
- if (node != null && File.Exists(node.Value))
- ParentPaths.Add(node.Value);
+ if (node != null && File.Exists(node.Value))
+ ParentPaths.Add(node.Value);
+ }
}
}
}
diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs
index a41b2dc0f..1c7bb8e0e 100644
--- a/Duplicati/Library/Utility/Utility.cs
+++ b/Duplicati/Library/Utility/Utility.cs
@@ -25,6 +25,7 @@ using System.Text.RegularExpressions;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Common;
using System.Globalization;
+using Duplicati.Library.Interface;
namespace Duplicati.Library.Utility
{
@@ -139,11 +140,50 @@ namespace Duplicati.Library.Utility
await target.WriteAsync(buf, 0, read, cancelToken).ConfigureAwait(false);
total += read;
}
-
+
return total;
}
/// <summary>
+ /// Get the length of a stream.
+ /// Attempt to use the stream's Position property if allowPositionFallback is <c>true</c> (only valid if stream is at the end).
+ /// </summary>
+ /// <param name="stream">Stream to get the length of.</param>
+ /// <param name="allowPositionFallback">Attempt to use the Position property if <c>true</c> and the Length property is not available (only valid if stream is at the end).</param>
+ /// <returns>Returns the stream's length, if available, or null if not supported by the stream.</returns>
+ public static long? GetStreamLength(Stream stream, bool allowPositionFallback = true)
+ {
+ return GetStreamLength(stream, out bool _, allowPositionFallback);
+ }
+
+ /// <summary>
+ /// Get the length of a stream.
+ /// Attempt to use the stream's Position property if allowPositionFallback is <c>true</c> (only valid if stream is at the end).
+ /// </summary>
+ /// <param name="stream">Stream to get the length of.</param>
+ /// <param name="isStreamPosition">Indicates if the Position value was used instead of Length.</param>
+ /// <param name="allowPositionFallback">Attempt to use the Position property if <c>true</c> and the Length property is not available (only valid if stream is at the end).</param>
+ /// <returns>Returns the stream's length, if available, or null if not supported by the stream.</returns>
+ public static long? GetStreamLength(Stream stream, out bool isStreamPosition, bool allowPositionFallback = true)
+ {
+ isStreamPosition = false;
+ long? streamLength = null;
+ try { streamLength = stream.Length; } catch { }
+ if (!streamLength.HasValue && allowPositionFallback)
+ {
+ try
+ {
+ // Hack: This is a fall-back method to detect the source stream size, assuming the current position is the end of the stream.
+ streamLength = stream.Position;
+ isStreamPosition = true;
+ }
+ catch { } //
+ }
+
+ return streamLength;
+ }
+
+ /// <summary>
/// These are characters that must be escaped when using a globbing expression
/// </summary>
private static readonly string BADCHARS = @"\\|\+|\||\{|\[|\(|\)|\]|\}|\^|\$|\#|\.";
diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj
index c0697798d..2daf6db29 100644
--- a/Duplicati/Server/Duplicati.Server.csproj
+++ b/Duplicati/Server/Duplicati.Server.csproj
@@ -168,6 +168,10 @@
<Project>{F61679A9-E5DE-468A-B5A4-05F92D0143D2}</Project>
<Name>Duplicati.Library.Backend.FTP</Name>
</ProjectReference>
+ <ProjectReference Include="..\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj">
+ <Project>{c16639f6-dacc-4dd9-86cd-8b937516b340}</Project>
+ <Name>Duplicati.Library.Backend.IDrive</Name>
+ </ProjectReference>
<ProjectReference Include="..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj">
<Project>{2cd5dbc3-3da6-432d-ba97-f0b8d24501c2}</Project>
<Name>Duplicati.Library.Backend.Jottacloud</Name>
diff --git a/Duplicati/Server/WebServer/Server.cs b/Duplicati/Server/WebServer/Server.cs
index 0ab45ba54..e9d5396e5 100644
--- a/Duplicati/Server/WebServer/Server.cs
+++ b/Duplicati/Server/WebServer/Server.cs
@@ -158,11 +158,23 @@ namespace Duplicati.Server.WebServer
// so we create a new server for each attempt
var server = CreateServer(options);
-
+
if (!certValid)
server.Start(listenInterface, p);
else
- server.Start(listenInterface, p, cert, System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12, null, false);
+ {
+ var secProtocols = System.Security.Authentication.SslProtocols.Tls12;
+
+ try
+ {
+ //try TLS 1.3 (type not available on .NET < 4.8)
+ secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288;
+ }
+ catch (NotSupportedException)
+ {
+ }
+ server.Start(listenInterface, p, cert, secProtocols, null, false);
+ }
m_server = server;
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
index 63a94b64d..c9abb3ebf 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js
@@ -25,14 +25,15 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.templates['gcs'] = 'templates/backends/gcs.html';
EditUriBackendConfig.templates['b2'] = 'templates/backends/b2.html';
EditUriBackendConfig.templates['mega'] = 'templates/backends/mega.html';
- EditUriBackendConfig.templates['jottacloud'] = 'templates/backends/jottacloud.html';
+ EditUriBackendConfig.templates['jottacloud'] = 'templates/backends/oauth.html';
+ EditUriBackendConfig.templates['idrive'] = 'templates/backends/idrive.html';
EditUriBackendConfig.templates['box'] = 'templates/backends/oauth.html';
- EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
- EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
- EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
+ EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
+ EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
+ EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
EditUriBackendConfig.templates['tardigrade'] = 'templates/backends/tardigrade.html';
- EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
- EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
+ EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
+ EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
EditUriBackendConfig.testers['s3'] = function(scope, callback) {
@@ -267,6 +268,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.loaders['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['msgroup'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.loaders['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -421,6 +423,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.parsers['onedrive'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.parsers['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -482,10 +485,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.mergeServerAndPath(scope);
};
- EditUriBackendConfig.parsers['jottacloud'] = function (scope, module, server, port, path, options) {
- EditUriBackendConfig.mergeServerAndPath(scope);
- };
-
EditUriBackendConfig.parsers['rclone'] = function (scope, module, server, port, path, options) {
if (options['--rclone-local-repository'])
scope.rclone_local_repository = options['--rclone-local-repository'];
@@ -643,6 +642,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.builders['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['sharepoint'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['msgroup'] = function() { return this['oauth-base'].apply(this, arguments); };
+ EditUriBackendConfig.builders['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -865,23 +865,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
}
- EditUriBackendConfig.builders['jottacloud'] = function (scope) {
- var opts = {};
-
- EditUriBackendConfig.merge_in_advanced_options(scope, opts);
-
- // Slightly better error message
- scope.Folder = scope.Path;
-
- var url = AppUtils.format('{0}://{1}{2}',
- scope.Backend.Key,
- scope.Path,
- AppUtils.encodeDictAsUrl(opts)
- );
-
- return url;
- };
-
EditUriBackendConfig.builders['cos'] = function (scope) {
var opts = {
@@ -948,6 +931,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.validaters['googledrive'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['gcs'] = EditUriBackendConfig.validaters['authid-base'];
+ EditUriBackendConfig.validaters['jottacloud'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['box'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['dropbox'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['onedrive'] = EditUriBackendConfig.validaters['authid-base'];
@@ -1157,16 +1141,6 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
continuation();
};
- EditUriBackendConfig.validaters['jottacloud'] = function (scope, continuation) {
- scope.Path = scope.Path || '';
- var res =
- EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) &&
- EditUriBackendConfig.require_field(scope, 'Password', gettextCatalog.getString('Password'));
-
- if (res)
- continuation();
- };
-
EditUriBackendConfig.validaters['sia'] = function (scope, continuation) {
var res =
EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Server'));
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
index b6b4e4e52..a05af144a 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js
@@ -66,6 +66,7 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ
'hubic': null,
'b2': null,
'mega': null,
+ 'idrive': null,
'box': null,
'od4b': null,
'mssp': null,
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/idrive.html b/Duplicati/Server/webroot/ngax/templates/backends/idrive.html
new file mode 100644
index 000000000..1a5973341
--- /dev/null
+++ b/Duplicati/Server/webroot/ngax/templates/backends/idrive.html
@@ -0,0 +1,13 @@
+<div class="input text">
+ <label for="idrive_path" translate>IDrive Sync directory path</label>
+ <input type="text" name="idrive_path" id="idrive_path" ng-model="$parent.Path" placeholder="{{'Enter directory path' | translate}}" />
+</div>
+
+<div class="input text">
+ <label for="idrive_username" translate>Username</label>
+ <input type="text" name="idrive_username" id="idrive_username" ng-model="$parent.Username" placeholder="{{'Username' | translate}}" />
+</div>
+<div class="input password">
+ <label for="idrive_password" translate>Password</label>
+ <input autocomplete="new-password" type="password" name="idrive_password" id="idrive_password" ng-model="$parent.Password" placeholder="{{'Password' | translate}}" />
+</div>
diff --git a/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
index 04efe6d28..dcff7dab5 100644
--- a/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
+++ b/Duplicati/Server/webroot/ngax/templates/backends/jottacloud.html
@@ -1,13 +1,12 @@
<div class="input text">
<label for="jottacloud_path" translate>Folder path</label>
- <input type="text" name="jottacloud_path" id="jottacloud_path" ng-model="$parent.Path" placeholder="{{'Enter folder path name' | translate}}" />
+ <input type="text" name="jottacloud_path" id="jottacloud_path" ng-model="$parent.Path" placeholder="{{'Enter the destination path' | translate}}" />
</div>
<div class="input text">
- <label for="jottacloud_username" translate>Username</label>
- <input type="text" name="jottacloud_username" id="jottacloud_username" ng-model="$parent.Username" placeholder="{{'Username' | translate}}" />
-</div>
-<div class="input password">
- <label for="jottacloud_password" translate>Password</label>
- <input autocomplete="new-password" type="password" name="jottacloud_password" id="jottacloud_password" ng-model="$parent.Password" placeholder="{{'Password' | translate}}" />
+ <label for="jottacloud_authid">
+ <a ng-show="oauth_in_progress" href="{{oauth_start_link}}" target="_blank" translate>AuthID</a>
+ <a ng-hide="oauth_in_progress" href ng-click="oauth_start_token_creation()" translate>AuthID</a>
+ </label>
+ <input type="text" name="jottacloud_authid" id="jottacloud_authid" ng-model="$parent.AuthID" placeholder="{{'Click the AuthID link to create an AuthID' | translate}}" />
</div>