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>2021-04-17 13:57:35 +0300
committerKenneth Skovhede <kenneth@hexad.dk>2021-04-17 13:57:35 +0300
commitfdcda53240ca174273efb079f1803298d7575e16 (patch)
tree7fc84826021b38822a021845b45c14fd9257e23b
parent4911cfdaa407a08b953dea1e1fd7531dcd5a8563 (diff)
WIP: Make all backends `async`.experiment/net5-split-cancellationtoken
All backends should support `CancellationToken` as well as pagination on list. Fully removed `HttpClient` and `WebRequest` Does not compile and needs some more work to be completed.
-rw-r--r--Duplicati/Library/Backend/AlternativeFTP/AlternativeFTPBackend.cs112
-rw-r--r--Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj1
-rw-r--r--Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs59
-rw-r--r--Duplicati/Library/Backend/AzureBlob/AzureBlobWrapper.cs35
-rw-r--r--Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj3
-rw-r--r--Duplicati/Library/Backend/Backblaze/B2.cs194
-rw-r--r--Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs131
-rw-r--r--Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj3
-rw-r--r--Duplicati/Library/Backend/Box/BoxBackend.cs122
-rw-r--r--Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj4
-rw-r--r--Duplicati/Library/Backend/CloudFiles/CloudFiles.cs166
-rw-r--r--Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj2
-rw-r--r--Duplicati/Library/Backend/CloudFiles/Strings.cs2
-rw-r--r--Duplicati/Library/Backend/Dropbox/Dropbox.cs47
-rw-r--r--Duplicati/Library/Backend/Dropbox/DropboxHelper.cs204
-rw-r--r--Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj2
-rw-r--r--Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj2
-rw-r--r--Duplicati/Library/Backend/FTP/FTPBackend.cs101
-rw-r--r--Duplicati/Library/Backend/FTP/Strings.cs2
-rw-r--r--Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj2
-rw-r--r--Duplicati/Library/Backend/File/FileBackend.cs90
-rw-r--r--Duplicati/Library/Backend/File/Strings.cs2
-rw-r--r--Duplicati/Library/Backend/File/Win32.cs2
-rw-r--r--Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs500
-rw-r--r--Duplicati/Library/Backend/OAuthHelper/MultipartItem.cs14
-rw-r--r--Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs113
-rw-r--r--Duplicati/Library/Backend/OAuthHelper/OAuthHttpMessageHandler.cs9
-rw-r--r--Duplicati/Library/Common/Platform/Platform.cs23
-rw-r--r--Duplicati/Library/Interface/BackendExtensions.cs36
-rw-r--r--Duplicati/Library/Interface/Duplicati.Library.Interface.csproj6
-rw-r--r--Duplicati/Library/Interface/IBackend.cs32
-rw-r--r--Duplicati/Library/Interface/IBackendPagination.cs17
-rw-r--r--Duplicati/Library/Interface/IStreamingBackend.cs48
-rw-r--r--Duplicati/Library/Utility/AsyncHttpRequest.cs293
34 files changed, 979 insertions, 1400 deletions
diff --git a/Duplicati/Library/Backend/AlternativeFTP/AlternativeFTPBackend.cs b/Duplicati/Library/Backend/AlternativeFTP/AlternativeFTPBackend.cs
index 0e027df83..a38fffb8c 100644
--- a/Duplicati/Library/Backend/AlternativeFTP/AlternativeFTPBackend.cs
+++ b/Duplicati/Library/Backend/AlternativeFTP/AlternativeFTPBackend.cs
@@ -30,12 +30,12 @@ using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
using CoreUtility = Duplicati.Library.Utility.Utility;
-using Uri = System.Uri;
-
+using Uri = System.Uri;
+
namespace Duplicati.Library.Backend.AlternativeFTP
{
// ReSharper disable once RedundantExtendsListEntry
- public class AlternativeFtpBackend : IBackend, IStreamingBackend
+ public class AlternativeFtpBackend : IBackend
{
private System.Net.NetworkCredential _userInfo;
private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash"; // Global option
@@ -200,44 +200,40 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
}
- public IEnumerable<IFileEntry> List()
- {
- return List("");
- }
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => ListAsync("", cancelToken);
- public IEnumerable<IFileEntry> List(string filename)
- {
- return List(filename, false);
- }
+ public Task<IList<IFileEntry>> ListAsync(string filename, CancellationToken cancelToken)
+ => ListAsync(filename, false, cancelToken);
- private IEnumerable<IFileEntry> List(string filename, bool stripFile)
+ private async Task<IList<IFileEntry>> ListAsync(string filename, bool stripFile, CancellationToken cancelToken)
{
var list = new List<IFileEntry>();
string remotePath = filename;
- try
- {
- var ftpClient = CreateClient();
+ var ftpClient = CreateClient();
- // Get the remote path
- var url = new Uri(this._url);
- remotePath = "/" + (url.AbsolutePath.EndsWith("/", StringComparison.Ordinal) ? url.AbsolutePath.Substring(0, url.AbsolutePath.Length - 1) : url.AbsolutePath);
+ // Get the remote path
+ var url = new Uri(this._url);
+ remotePath = "/" + (url.AbsolutePath.EndsWith("/", StringComparison.Ordinal) ? url.AbsolutePath.Substring(0, url.AbsolutePath.Length - 1) : url.AbsolutePath);
- if (!string.IsNullOrEmpty(filename))
+ if (!string.IsNullOrEmpty(filename))
+ {
+ if (!stripFile)
{
- if (!stripFile)
- {
- // Append the filename
- remotePath += filename;
- }
- else if (filename.Contains("/"))
- {
- remotePath += filename.Substring(0, filename.LastIndexOf("/", StringComparison.Ordinal));
- }
- // else: stripping the filename in this case ignoring it
+ // Append the filename
+ remotePath += filename;
}
-
- foreach (FtpListItem item in ftpClient.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
+ else if (filename.Contains("/"))
+ {
+ remotePath += filename.Substring(0, filename.LastIndexOf("/", StringComparison.Ordinal));
+ }
+ // else: stripping the filename in this case ignoring it
+ }
+
+ try
+ {
+ foreach (var item in await ftpClient.GetListingAsync(remotePath, FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks, cancelToken))
{
switch (item.Type)
{
@@ -299,9 +295,12 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
}
- }// Message "Directory not found." string
+
+ return list;
+ }
catch (FtpCommandException ex)
{
+ // Message "Directory not found." string
if (ex.Message == "Directory not found.")
{
throw new FolderMissingException(Strings.MissingFolderError(remotePath, ex.Message), ex);
@@ -310,7 +309,6 @@ namespace Duplicati.Library.Backend.AlternativeFTP
throw;
}
- return list;
}
public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken)
@@ -370,15 +368,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
}
- public Task PutAsync(string remotename, string localname, CancellationToken cancelToken)
- {
- using (FileStream fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- return PutAsync(remotename, fs, cancelToken);
- }
- }
-
- public void Get(string remotename, Stream output)
+ public async Task GetAsync(string remotename, Stream output, CancellationToken cancelToken)
{
var ftpClient = CreateClient();
@@ -391,11 +381,11 @@ namespace Duplicati.Library.Backend.AlternativeFTP
remotePath += remotename;
}
- using (var inputStream = ftpClient.OpenRead(remotePath))
+ using (var inputStream = await ftpClient.OpenReadAsync(remotePath, cancelToken))
{
try
{
- CoreUtility.CopyStream(inputStream, output, false, _copybuffer);
+ await CoreUtility.CopyStreamAsync(inputStream, output, false, cancelToken, _copybuffer);
}
finally
{
@@ -405,15 +395,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
- public void Get(string remotename, string localname)
- {
- using (FileStream fs = File.Open(localname, FileMode.Create, FileAccess.Write, FileShare.None))
- {
- Get(remotename, fs);
- }
- }
-
- public void Delete(string remotename)
+ public Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
var ftpClient = CreateClient();
@@ -426,8 +408,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
remotePath += remotename;
}
- ftpClient.DeleteFile(remotePath);
-
+ return ftpClient.DeleteFileAsync(remotePath, cancelToken);
}
/// <summary>
@@ -446,6 +427,8 @@ namespace Duplicati.Library.Backend.AlternativeFTP
get { return new string[] { new Uri(_url).Host }; }
}
+ public bool SupportsStreaming => true;
+
private static Stream StringToStream(string str)
{
var stream = new MemoryStream();
@@ -457,16 +440,14 @@ namespace Duplicati.Library.Backend.AlternativeFTP
/// <summary>
/// Test FTP access permissions.
/// </summary>
- public void Test()
+ public async Task TestAsync(CancellationToken cancelToken)
{
- var list = List();
-
// Delete test file if exists
- if (list.Any(entry => entry.Name == TEST_FILE_NAME))
+ if ((await ListAsync(cancelToken)).Any(x => x.Name == TEST_FILE_NAME))
{
try
{
- Delete(TEST_FILE_NAME);
+ await DeleteAsync(TEST_FILE_NAME, cancelToken);
}
catch (Exception e)
{
@@ -479,7 +460,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
{
try
{
- PutAsync(TEST_FILE_NAME, testStream, CancellationToken.None).Wait();
+ await PutAsync(TEST_FILE_NAME, testStream, cancelToken);
}
catch (Exception e)
{
@@ -492,7 +473,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
{
try
{
- Get(TEST_FILE_NAME, stream);
+ await GetAsync(TEST_FILE_NAME, stream, cancelToken);
}
catch (Exception e)
{
@@ -503,7 +484,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
// Cleanup
try
{
- Delete(TEST_FILE_NAME);
+ await DeleteAsync(TEST_FILE_NAME, cancelToken);
}
catch (Exception e)
{
@@ -511,7 +492,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
}
- public void CreateFolder()
+ public Task CreateFolderAsync(CancellationToken cancelToken)
{
var client = CreateClient();
@@ -521,8 +502,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
var remotePath = url.AbsolutePath.EndsWith("/", StringComparison.Ordinal) ? url.AbsolutePath.Substring(0, url.AbsolutePath.Length - 1) : url.AbsolutePath;
// Try to create the directory
- client.CreateDirectory(remotePath, true);
-
+ return client.CreateDirectoryAsync(remotePath, true, cancelToken);
}
public void Dispose()
diff --git a/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj b/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj
index aa3121d96..e47c0fd6d 100644
--- a/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj
+++ b/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj
@@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>An alternative FTP backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.AlternativeFTP</RootNamespace>
</PropertyGroup>
<ItemGroup>
diff --git a/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs b/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs
index 42d6d7725..49982abd9 100644
--- a/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs
+++ b/Duplicati/Library/Backend/AzureBlob/AzureBlobBackend.cs
@@ -28,7 +28,7 @@ namespace Duplicati.Library.Backend.AzureBlob
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
- public class AzureBlobBackend : IStreamingBackend
+ public class AzureBlobBackend : IBackend
{
private readonly AzureBlobWrapper _azureBlob;
@@ -84,44 +84,17 @@ namespace Duplicati.Library.Backend.AzureBlob
get { return "azure"; }
}
- public IEnumerable<IFileEntry> List()
- {
- return _azureBlob.ListContainerEntries();
- }
+ public async Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => await _azureBlob.ListContainerEntriesAsync(cancelToken);
- public Task PutAsync(string remotename, string localname, CancellationToken cancelToken)
- {
- using (var fs = File.Open(localname,
- FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- return PutAsync(remotename, fs, cancelToken);
- }
- }
+ public Task PutAsync(string remotename, Stream input, CancellationToken cancelToken)
+ => _azureBlob.AddFileStream(remotename, input, cancelToken);
- public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken)
- {
- await _azureBlob.AddFileStream(remotename, input, cancelToken);
- }
+ public Task GetAsync(string remotename, Stream output, CancellationToken cancelToken)
+ => _azureBlob.GetFileStreamAsync(remotename, output, cancelToken);
- public void Get(string remotename, string localname)
- {
- using (var fs = File.Open(localname,
- FileMode.Create, FileAccess.Write,
- FileShare.None))
- {
- Get(remotename, fs);
- }
- }
-
- public void Get(string remotename, Stream output)
- {
- _azureBlob.GetFileStream(remotename, output);
- }
-
- public void Delete(string remotename)
- {
- _azureBlob.DeleteObject(remotename);
- }
+ public Task DeleteAsync(string remotename, CancellationToken cancelToken)
+ => _azureBlob.DeleteObjectAsync(remotename, cancelToken);
public IList<ICommandLineArgument> SupportedCommands
{
@@ -181,15 +154,13 @@ namespace Duplicati.Library.Backend.AzureBlob
get { return _azureBlob.DnsNames; }
}
- public void Test()
- {
- this.TestList();
- }
+ public bool SupportsStreaming => true;
- public void CreateFolder()
- {
- _azureBlob.AddContainer();
- }
+ public Task TestAsync(CancellationToken cancelToken)
+ => this.TestListAsync(cancelToken);
+
+ public Task CreateFolderAsync(CancellationToken cancelToken)
+ => _azureBlob.AddContainerAsync(cancelToken);
public void Dispose()
{
diff --git a/Duplicati/Library/Backend/AzureBlob/AzureBlobWrapper.cs b/Duplicati/Library/Backend/AzureBlob/AzureBlobWrapper.cs
index ad6db77d3..0a3df3fb5 100644
--- a/Duplicati/Library/Backend/AzureBlob/AzureBlobWrapper.cs
+++ b/Duplicati/Library/Backend/AzureBlob/AzureBlobWrapper.cs
@@ -83,30 +83,25 @@ namespace Duplicati.Library.Backend.AzureBlob
_container = blobClient.GetContainerReference(containerName);
}
- public void AddContainer()
+ public async Task AddContainerAsync(CancellationToken cancelToken)
{
- _container.CreateAsync().GetAwaiter().GetResult();
- _container.SetPermissionsAsync(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Off });
+ await _container.CreateAsync(default(BlobContainerPublicAccessType), default(BlobRequestOptions), new OperationContext(), cancelToken);
+ await _container.SetPermissionsAsync(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Off }, default(AccessCondition), default(BlobRequestOptions), new OperationContext(), cancelToken);
}
- public virtual void GetFileStream(string keyName, Stream target)
- {
- _container.GetBlockBlobReference(keyName).DownloadToStreamAsync(target).GetAwaiter().GetResult();
- }
+ public virtual Task GetFileStreamAsync(string keyName, Stream target, CancellationToken cancelToken)
+ => _container.GetBlockBlobReference(keyName).DownloadToStreamAsync(target, default(AccessCondition), default(BlobRequestOptions), new OperationContext(), cancelToken);
- public virtual async Task AddFileStream(string keyName, Stream source, CancellationToken cancelToken)
- {
- await _container.GetBlockBlobReference(keyName).UploadFromStreamAsync(source, source.Length, default(AccessCondition), default(BlobRequestOptions), new OperationContext(), cancelToken);
- }
- public void DeleteObject(string keyName)
- {
- _container.GetBlockBlobReference(keyName).DeleteIfExistsAsync().GetAwaiter().GetResult();
- }
+ public virtual Task AddFileStream(string keyName, Stream source, CancellationToken cancelToken)
+ => _container.GetBlockBlobReference(keyName).UploadFromStreamAsync(source, source.Length, default(AccessCondition), default(BlobRequestOptions), new OperationContext(), cancelToken);
+
+ public Task DeleteObjectAsync(string keyName, CancellationToken cancelToken)
+ => _container.GetBlockBlobReference(keyName).DeleteIfExistsAsync(default(DeleteSnapshotsOption), default(AccessCondition), default(BlobRequestOptions), new OperationContext(), cancelToken);
- private async Task<List<IListBlobItem>> ListContainerEntriesAsync()
+ private async Task<List<IListBlobItem>> ListBlobEntriesAsync(CancellationToken cancelToken)
{
- var segment = await _container.ListBlobsSegmentedAsync(null);
+ var segment = await _container.ListBlobsSegmentedAsync(null, false, default(BlobListingDetails), null, null, default(BlobRequestOptions), new OperationContext(), cancelToken);
var list = new List<IListBlobItem>();
list.AddRange(segment.Results);
@@ -114,16 +109,16 @@ namespace Duplicati.Library.Backend.AzureBlob
while (segment.ContinuationToken != null)
{
// TODO-DNC do we need BlobListingDetails.Metadata ???
- segment = await _container.ListBlobsSegmentedAsync(segment.ContinuationToken);
+ segment = await _container.ListBlobsSegmentedAsync(null, false, default(BlobListingDetails), null, segment.ContinuationToken, default(BlobRequestOptions), new OperationContext(), cancelToken);
list.AddRange(segment.Results);
}
return list;
}
- public virtual List<IFileEntry> ListContainerEntries()
+ public virtual async Task<List<IFileEntry>> ListContainerEntriesAsync(CancellationToken cancelToken)
{
- var listBlobItems = ListContainerEntriesAsync().GetAwaiter().GetResult();
+ var listBlobItems = await ListBlobEntriesAsync(cancelToken);
try
{
return listBlobItems.Select(x =>
diff --git a/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj b/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj
index 7807d49a5..32e13562d 100644
--- a/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj
+++ b/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj
@@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>An Azure blob storage backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.AzureBlob</RootNamespace>
</PropertyGroup>
<PropertyGroup>
@@ -26,7 +27,7 @@
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
</ItemGroup>
-
+
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
diff --git a/Duplicati/Library/Backend/Backblaze/B2.cs b/Duplicati/Library/Backend/Backblaze/B2.cs
index 50fb72659..c0805d40b 100644
--- a/Duplicati/Library/Backend/Backblaze/B2.cs
+++ b/Duplicati/Library/Backend/Backblaze/B2.cs
@@ -1,4 +1,6 @@
-// Copyright (C) 2015, The Duplicati Team
+using System.Net.Http.Headers;
+using System.Net.Http;
+// 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
@@ -28,13 +30,13 @@ using System.Threading.Tasks;
namespace Duplicati.Library.Backend.Backblaze
{
- public class B2 : IBackend, IStreamingBackend
+ public class B2 : IBackend
{
private const string B2_ID_OPTION = "b2-accountid";
- private const string B2_KEY_OPTION = "b2-applicationkey";
- private const string B2_PAGESIZE_OPTION = "b2-page-size";
+ private const string B2_KEY_OPTION = "b2-applicationkey";
+ private const string B2_PAGESIZE_OPTION = "b2-page-size";
private const string B2_DOWNLOAD_URL_OPTION = "b2-download-url";
-
+
private const string B2_CREATE_BUCKET_TYPE_OPTION = "b2-create-bucket-type";
private const string DEFAULT_BUCKET_TYPE = "allPrivate";
@@ -96,9 +98,9 @@ namespace Duplicati.Library.Backend.Backblaze
if (string.IsNullOrEmpty(accountKey))
throw new UserInformationException(Strings.B2.NoB2KeyError, "B2MissingKey");
- m_helper = new B2AuthHelper(accountId, accountKey);
-
- m_pagesize = DEFAULT_PAGE_SIZE;
+ m_helper = new B2AuthHelper(accountId, accountKey);
+
+ m_pagesize = DEFAULT_PAGE_SIZE;
if (options.ContainsKey(B2_PAGESIZE_OPTION))
{
int.TryParse(options[B2_PAGESIZE_OPTION], out m_pagesize);
@@ -114,63 +116,60 @@ namespace Duplicati.Library.Backend.Backblaze
}
}
- private BucketEntity Bucket
+ private async Task<BucketEntity> GetBucketAsync(CancellationToken cancelToken)
{
- get
+ if (m_bucket == null)
{
- if (m_bucket == null)
- {
- var buckets = m_helper.PostAndGetJSONData<ListBucketsResponse>(
- string.Format("{0}/b2api/v1/b2_list_buckets", m_helper.APIUrl),
- new ListBucketsRequest() {
- AccountID = m_helper.AccountID
- }
- );
+ var buckets = await m_helper.PostAndGetJSONDataAsync<ListBucketsResponse>(
+ string.Format("{0}/b2api/v1/b2_list_buckets", await m_helper.GetAPIUrlAsync(cancelToken)),
+ new ListBucketsRequest() {
+ AccountID = await m_helper.GetAccountIDAsync(cancelToken)
+ },
+ null,
+ cancelToken
+ );
- if (buckets != null && buckets.Buckets != null)
- m_bucket = buckets.Buckets.FirstOrDefault(x => string.Equals(x.BucketName, m_bucketname, StringComparison.OrdinalIgnoreCase));
+ if (buckets != null && buckets.Buckets != null)
+ m_bucket = buckets.Buckets.FirstOrDefault(x => string.Equals(x.BucketName, m_bucketname, StringComparison.OrdinalIgnoreCase));
- if (m_bucket == null)
- throw new FolderMissingException();
- }
-
- return m_bucket;
+ if (m_bucket == null)
+ throw new FolderMissingException();
}
+
+ return m_bucket;
}
- private UploadUrlResponse UploadUrlData
+ private async Task<UploadUrlResponse> GetUploadUrlDataAsync(CancellationToken cancelToken)
{
- get
- {
- if (m_uploadUrl == null)
- m_uploadUrl = m_helper.PostAndGetJSONData<UploadUrlResponse>(
- string.Format("{0}/b2api/v1/b2_get_upload_url", m_helper.APIUrl),
- new UploadUrlRequest() { BucketID = Bucket.BucketID }
- );
+ if (m_uploadUrl == null)
+ m_uploadUrl = await m_helper.PostAndGetJSONDataAsync<UploadUrlResponse>(
+ string.Format("{0}/b2api/v1/b2_get_upload_url", await m_helper.GetAPIUrlAsync(cancelToken)),
+ new UploadUrlRequest() { BucketID = (await GetBucketAsync(cancelToken)).BucketID },
+ null,
+ cancelToken
+ );
- return m_uploadUrl;
- }
+ return m_uploadUrl;
}
- private string GetFileID(string filename)
+ private async Task<string> GetFileIDAsync(string filename, CancellationToken cancelToken)
{
if (m_filecache != null && m_filecache.ContainsKey(filename))
return m_filecache[filename].OrderByDescending(x => x.UploadTimestamp).First().FileID;
- List();
+ await ListAsync(cancelToken);
+
if (m_filecache.ContainsKey(filename))
return m_filecache[filename].OrderByDescending(x => x.UploadTimestamp).First().FileID;
throw new FileMissingException();
}
- private string DownloadUrl {
- get {
- if (string.IsNullOrEmpty(m_downloadUrl)) {
- return m_helper.DownloadUrl;
- } else {
- return m_downloadUrl;
- }
+ private async Task<string> GetDownloadUrlAsync(CancellationToken cancelToken) {
+ if (string.IsNullOrEmpty(m_downloadUrl)) {
+ return await m_helper.GetDownloadUrlAsync(cancelToken);
+ } else {
+ return m_downloadUrl;
}
}
@@ -183,8 +182,8 @@ namespace Duplicati.Library.Backend.Backblaze
new CommandLineArgument(B2_KEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.B2.B2applicationkeyDescriptionShort, Strings.B2.B2applicationkeyDescriptionLong, null, new string[] {"auth-username"}, null),
new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.B2.AuthPasswordDescriptionShort, Strings.B2.AuthPasswordDescriptionLong),
new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.B2.AuthUsernameDescriptionShort, Strings.B2.AuthUsernameDescriptionLong),
- new CommandLineArgument(B2_CREATE_BUCKET_TYPE_OPTION, CommandLineArgument.ArgumentType.String, Strings.B2.B2createbuckettypeDescriptionShort, Strings.B2.B2createbuckettypeDescriptionLong, DEFAULT_BUCKET_TYPE),
- new CommandLineArgument(B2_PAGESIZE_OPTION, CommandLineArgument.ArgumentType.Integer, Strings.B2.B2pagesizeDescriptionShort, Strings.B2.B2pagesizeDescriptionLong, DEFAULT_PAGE_SIZE.ToString()),
+ new CommandLineArgument(B2_CREATE_BUCKET_TYPE_OPTION, CommandLineArgument.ArgumentType.String, Strings.B2.B2createbuckettypeDescriptionShort, Strings.B2.B2createbuckettypeDescriptionLong, DEFAULT_BUCKET_TYPE),
+ new CommandLineArgument(B2_PAGESIZE_OPTION, CommandLineArgument.ArgumentType.Integer, Strings.B2.B2pagesizeDescriptionShort, Strings.B2.B2pagesizeDescriptionLong, DEFAULT_PAGE_SIZE.ToString()),
new CommandLineArgument(B2_DOWNLOAD_URL_OPTION, CommandLineArgument.ArgumentType.String, Strings.B2.B2downloadurlDescriptionShort, Strings.B2.B2downloadurlDescriptionLong),
});
@@ -229,33 +228,30 @@ namespace Duplicati.Library.Backend.Backblaze
}
if (m_filecache == null)
- List();
+ await ListAsync(cancelToken);
try
{
+ var updata = await GetUploadUrlDataAsync(cancelToken);
var fileinfo = await m_helper.GetJSONDataAsync<UploadFileResponse>(
- UploadUrlData.UploadUrl,
- cancelToken,
+ updata.UploadUrl,
req =>
{
- req.Method = "POST";
- req.Headers["Authorization"] = UploadUrlData.AuthorizationToken;
- req.Headers["X-Bz-Content-Sha1"] = sha1;
- req.Headers["X-Bz-File-Name"] = m_urlencodedprefix + Utility.Uri.UrlPathEncode(remotename);
- req.ContentType = "application/octet-stream";
- req.ContentLength = stream.Length;
+ req.Method = new HttpMethod("POST");
+ req.Headers.Add("Authorization", updata.AuthorizationToken);
+ req.Headers.Add("X-Bz-Content-Sha1", sha1);
+ req.Headers.Add("X-Bz-File-Name", m_urlencodedprefix + Utility.Uri.UrlPathEncode(remotename));
+ var body = new StreamContent(stream);
+ body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
+ body.Headers.ContentLength = stream.Length;
+ req.Content = body;
},
-
- async (req, reqCancelToken) =>
- {
- using (var rs = req.GetRequestStream())
- await Utility.Utility.CopyStreamAsync(stream, rs, reqCancelToken);
- }
+ cancelToken
).ConfigureAwait(false);
// Delete old versions
if (m_filecache.ContainsKey(remotename))
- Delete(remotename);
+ await DeleteAsync(remotename, cancelToken).ConfigureAwait(false);;
m_filecache[remotename] = new List<FileEntity>();
m_filecache[remotename].Add(new FileEntity() {
@@ -282,35 +278,34 @@ namespace Duplicati.Library.Backend.Backblaze
}
}
- public void Get(string remotename, System.IO.Stream stream)
+ public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
- AsyncHttpRequest req;
+ HttpRequestMessage req;
if (m_filecache == null || !m_filecache.ContainsKey(remotename))
- List();
+ await ListAsync(cancelToken);
if (m_filecache != null && m_filecache.ContainsKey(remotename))
- req = new AsyncHttpRequest(m_helper.CreateRequest(string.Format("{0}/b2api/v1/b2_download_file_by_id?fileId={1}", DownloadUrl, Library.Utility.Uri.UrlEncode(GetFileID(remotename)))));
+ req = await m_helper.CreateRequestAsync(string.Format("{0}/b2api/v1/b2_download_file_by_id?fileId={1}", await GetDownloadUrlAsync(cancelToken), Library.Utility.Uri.UrlEncode(await GetFileIDAsync(remotename, cancelToken))), null , cancelToken);
else
- req = new AsyncHttpRequest(m_helper.CreateRequest(string.Format("{0}/{1}{2}", DownloadUrl, m_urlencodedprefix, Library.Utility.Uri.UrlPathEncode(remotename))));
+ req = await m_helper.CreateRequestAsync(string.Format("{0}/{1}{2}", await GetDownloadUrlAsync(cancelToken), m_urlencodedprefix, Library.Utility.Uri.UrlPathEncode(remotename)), null, cancelToken);
try
{
- using(var resp = req.GetResponse())
- using(var rs = req.GetResponseStream())
- Library.Utility.Utility.CopyStream(rs, stream);
+ using(var resp = await m_helper.SendAsync(req, cancelToken))
+ await Duplicati.Library.Utility.Utility.CopyStreamAsync(await resp.Content.ReadAsStreamAsync(), stream, cancelToken);
}
catch (Exception ex)
{
if (B2AuthHelper.GetExceptionStatusCode(ex) == HttpStatusCode.NotFound)
throw new FileMissingException();
- B2AuthHelper.AttemptParseAndThrowException(ex);
+ await B2AuthHelper.AttemptParseAndThrowExceptionAsync(ex);
throw;
}
}
- public IEnumerable<IFileEntry> List()
+ public async Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
{
m_filecache = null;
var cache = new Dictionary<string, List<FileEntity>>();
@@ -318,15 +313,17 @@ namespace Duplicati.Library.Backend.Backblaze
string nextFileName = null;
do
{
- var resp = m_helper.PostAndGetJSONData<ListFilesResponse>(
- string.Format("{0}/b2api/v1/b2_list_file_versions", m_helper.APIUrl),
+ var resp = await m_helper.PostAndGetJSONDataAsync<ListFilesResponse>(
+ string.Format("{0}/b2api/v1/b2_list_file_versions", await m_helper.GetAPIUrlAsync(cancelToken)),
new ListFilesRequest() {
- BucketID = Bucket.BucketID,
+ BucketID = (await GetBucketAsync(cancelToken)).BucketID,
MaxFileCount = m_pagesize,
Prefix = m_prefix,
StartFileID = nextFileID,
StartFileName = nextFileName
- }
+ },
+ null,
+ cancelToken
);
nextFileID = resp.NextFileID;
@@ -364,35 +361,25 @@ namespace Duplicati.Library.Backend.Backblaze
).ToList();
}
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
- {
- using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
- return PutAsync(remotename, fs, cancelToken);
- }
-
- public void Get(string remotename, string filename)
- {
- using (System.IO.FileStream fs = System.IO.File.Create(filename))
- Get(remotename, fs);
- }
-
- public void Delete(string remotename)
+ public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
try
{
if (m_filecache == null || !m_filecache.ContainsKey(remotename))
- List();
+ await ListAsync(cancelToken);
if (!m_filecache.ContainsKey(remotename))
throw new FileMissingException();
foreach(var n in m_filecache[remotename].OrderBy(x => x.UploadTimestamp))
- m_helper.PostAndGetJSONData<DeleteResponse>(
- string.Format("{0}/b2api/v1/b2_delete_file_version", m_helper.APIUrl),
+ await m_helper.PostAndGetJSONDataAsync<DeleteResponse>(
+ string.Format("{0}/b2api/v1/b2_delete_file_version", await m_helper.GetAPIUrlAsync(cancelToken)),
new DeleteRequest() {
FileName = m_prefix + remotename,
FileID = n.FileID
- }
+ },
+ null,
+ cancelToken
);
m_filecache[remotename].Clear();
@@ -404,20 +391,20 @@ namespace Duplicati.Library.Backend.Backblaze
}
}
- public void Test()
- {
- this.TestList();
- }
+ public Task TestAsync(CancellationToken cancelToken)
+ => this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public async Task CreateFolderAsync(CancellationToken cancelToken)
{
- m_bucket = m_helper.PostAndGetJSONData<BucketEntity>(
- string.Format("{0}/b2api/v1/b2_create_bucket", m_helper.APIUrl),
+ m_bucket = await m_helper.PostAndGetJSONDataAsync<BucketEntity>(
+ string.Format("{0}/b2api/v1/b2_create_bucket", await m_helper.GetAPIUrlAsync(cancelToken)),
new BucketEntity() {
- AccountID = m_helper.AccountID,
+ AccountID = await m_helper.GetAccountIDAsync(cancelToken),
BucketName = m_bucketname,
BucketType = m_bucketType
- }
+ },
+ null,
+ cancelToken
);
}
@@ -441,6 +428,9 @@ namespace Duplicati.Library.Backend.Backblaze
get { return new string[] { new System.Uri(B2AuthHelper.AUTH_URL).Host, m_helper?.APIDnsName, m_helper?.DownloadDnsName} ; }
}
+ public bool SupportsStreaming => true;
+
+
public void Dispose()
{
}
diff --git a/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs b/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs
index a358924ed..51348f3be 100644
--- a/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs
+++ b/Duplicati/Library/Backend/Backblaze/B2AuthHelper.cs
@@ -1,4 +1,5 @@
-// Copyright (C) 2015, The Duplicati Team
+using System.Threading;
+// 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
@@ -15,10 +16,12 @@
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
-using System.Net;
+using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Duplicati.Library.Utility;
+using System.Net;
+using System.Threading.Tasks;
namespace Duplicati.Library.Backend.Backblaze
{
@@ -35,17 +38,21 @@ namespace Duplicati.Library.Backend.Backblaze
m_credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(userid + ":" + password));
}
- public override HttpWebRequest CreateRequest(string url, string method = null)
+ public override async Task<HttpRequestMessage> CreateRequestAsync(string url, string method, CancellationToken cancelToken)
{
- var r = base.CreateRequest(url, method);
- r.Headers["Authorization"] = AuthorizationToken;
+ var r = await base.CreateRequestAsync(url, method, cancelToken);
+ r.Headers.Add("Authorization", await GetAuthorizationTokenAsync(cancelToken));
return r;
}
- public string AuthorizationToken { get { return Config.AuthorizationToken; } }
- public string APIUrl { get { return Config.APIUrl; } }
- public string DownloadUrl { get { return Config.DownloadUrl; } }
- public string AccountID { get { return Config.AccountID; } }
+ public async Task<string> GetAuthorizationTokenAsync(CancellationToken cancelToken)
+ => (await GetConfigAsync(cancelToken)).AuthorizationToken;
+ public async Task<string> GetAPIUrlAsync(CancellationToken cancelToken)
+ => (await GetConfigAsync(cancelToken)).APIUrl;
+ public async Task<string> GetDownloadUrlAsync(CancellationToken cancelToken)
+ => (await GetConfigAsync(cancelToken)).DownloadUrl;
+ public async Task<string> GetAccountIDAsync(CancellationToken cancelToken)
+ => (await GetConfigAsync(cancelToken)).AccountID;
private string DropTrailingSlashes(string url)
{
@@ -74,76 +81,69 @@ namespace Duplicati.Library.Backend.Backblaze
}
}
- private AuthResponse Config
+ private async Task<AuthResponse> GetConfigAsync(CancellationToken cancelToken)
{
- get
+ if (m_config == null || m_configExpires < DateTime.UtcNow)
{
- if (m_config == null || m_configExpires < DateTime.UtcNow)
- {
- var retries = 0;
+ var retries = 0;
- while(true)
+ while(true)
+ {
+ try
{
- try
- {
- var req = base.CreateRequest(AUTH_URL);
- req.Headers.Add("Authorization", string.Format("Basic {0}", m_credentials));
- req.ContentType = "application/json; charset=utf-8";
-
- using(var resp = (HttpWebResponse)new AsyncHttpRequest(req).GetResponse())
- m_config = ReadJSONResponse<AuthResponse>(resp);
-
- m_config.APIUrl = DropTrailingSlashes(m_config.APIUrl);
- m_config.DownloadUrl = DropTrailingSlashes(m_config.DownloadUrl);
+ var req = await base.CreateRequestAsync(AUTH_URL, null, cancelToken);
+ req.Headers.Add("Authorization", string.Format("Basic {0}", m_credentials));
+ //req.ContentType = "application/json; charset=utf-8";
+
+ using(var resp = await m_client.SendAsync(req, cancelToken))
+ m_config = await ReadJSONResponseAsync<AuthResponse>(resp, cancelToken);
+
+ m_config.APIUrl = DropTrailingSlashes(m_config.APIUrl);
+ m_config.DownloadUrl = DropTrailingSlashes(m_config.DownloadUrl);
+
+ m_configExpires = DateTime.UtcNow + TimeSpan.FromHours(1);
+ return m_config;
+ }
+ catch (Exception ex)
+ {
+ var clienterror = false;
- m_configExpires = DateTime.UtcNow + TimeSpan.FromHours(1);
- return m_config;
- }
- catch (Exception ex)
+ try
{
- var clienterror = false;
-
- try
- {
- // Only retry once on client errors
- if (ex is WebException exception && exception.Response is HttpWebResponse response)
- {
- var sc = (int)response.StatusCode;
- clienterror = (sc >= 400 && sc <= 499);
- }
- }
- catch
- {
- }
-
- if (retries >= (clienterror ? 1 : 5))
+ // Only retry once on client errors
+ if (ex is HttpRequestStatusException exception)
{
- AttemptParseAndThrowException(ex);
- throw;
+ var sc = (int)exception.Response.StatusCode;
+ clienterror = (sc >= 400 && sc <= 499);
}
+ }
+ catch
+ {
+ }
- System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
- retries++;
+ if (retries >= (clienterror ? 1 : 5))
+ {
+ await AttemptParseAndThrowExceptionAsync(ex);
+ throw;
}
+
+ System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
+ retries++;
}
}
-
- return m_config;
}
+
+ return m_config;
}
- public static void AttemptParseAndThrowException(Exception ex)
+ public static async Task AttemptParseAndThrowExceptionAsync(Exception ex)
{
Exception newex = null;
try
{
- if (ex is WebException exception && exception.Response is HttpWebResponse hs)
+ if (ex is HttpRequestStatusException exception)
{
- string rawdata = null;
- using(var rs = Library.Utility.AsyncHttpRequest.TrySetTimeout(hs.GetResponseStream()))
- using(var sr = new System.IO.StreamReader(rs))
- rawdata = sr.ReadToEnd();
-
+ var rawdata = await exception.Response.Content.ReadAsStringAsync();
newex = new Exception("Raw message: " + rawdata);
var msg = JsonConvert.DeserializeObject<ErrorResponse>(rawdata);
@@ -158,18 +158,19 @@ namespace Duplicati.Library.Backend.Backblaze
throw newex;
}
- protected override void ParseException(Exception ex)
- {
- AttemptParseAndThrowException(ex);
- }
+ protected override Task ParseExceptionAsync(Exception ex)
+ => AttemptParseAndThrowExceptionAsync(ex);
public static HttpStatusCode GetExceptionStatusCode(Exception ex)
{
- if (ex is WebException exception && exception.Response is HttpWebResponse response)
- return response.StatusCode;
+ if (ex is HttpRequestStatusException exception)
+ return exception.Response.StatusCode;
else
return default(HttpStatusCode);
}
+
+ public Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, CancellationToken cancelToken)
+ => m_client.SendAsync(requestMessage, cancelToken);
private class ErrorResponse
diff --git a/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
index 6dc28bfde..c151ca435 100644
--- a/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
+++ b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj
@@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>Backblaze backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.Backblaze</RootNamespace>
</PropertyGroup>
<ItemGroup>
@@ -16,7 +17,7 @@
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj" />
</ItemGroup>
-
+
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
diff --git a/Duplicati/Library/Backend/Box/BoxBackend.cs b/Duplicati/Library/Backend/Box/BoxBackend.cs
index 03d6f6bb0..639cd2b5f 100644
--- a/Duplicati/Library/Backend/Box/BoxBackend.cs
+++ b/Duplicati/Library/Backend/Box/BoxBackend.cs
@@ -28,7 +28,7 @@ namespace Duplicati.Library.Backend.Box
{
// ReSharper disable once ClassNeverInstantiated.Global
// This class is instantiated dynamically in the BackendLoader.
- public class BoxBackend : IBackend, IStreamingBackend
+ public class BoxBackend : IBackend, IBackendPagination
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType<BoxBackend>();
@@ -55,17 +55,14 @@ namespace Duplicati.Library.Backend.Box
AutoAuthHeader = true;
}
- protected override void ParseException(Exception ex)
+ protected override async Task ParseExceptionAsync(Exception ex)
{
Exception newex = null;
try
{
- if (ex is WebException exception && exception.Response is HttpWebResponse hs)
+ if (ex is HttpRequestStatusException exception)
{
- string rawdata = null;
- using(var rs = Library.Utility.AsyncHttpRequest.TrySetTimeout(hs.GetResponseStream()))
- using(var sr = new System.IO.StreamReader(rs))
- rawdata = sr.ReadToEnd();
+ var rawdata = await exception.Response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(rawdata))
return;
@@ -113,32 +110,53 @@ namespace Duplicati.Library.Backend.Box
m_oauth = new BoxHelper(authid);
}
- private string CurrentFolder
+ private async Task<string> GetCurrentFolderAsync(CancellationToken cancelToken)
{
- get
- {
- if (m_currentfolder == null)
- GetCurrentFolder(false);
-
- return m_currentfolder;
- }
+ if (m_currentfolder == null)
+ await GetCurrentFolderAsync(false, cancelToken);
+
+ return m_currentfolder;
+ }
+
+ // Include the async enumerable library instead?
+
+ private static async Task<T> FirstOrDefaultAsync<T>(IAsyncEnumerable<T> src, Func<T, bool> predicate = null)
+ {
+ await foreach(var x in src)
+ if (predicate == null || predicate(x))
+ return x;
+
+ return default(T);
}
- private void GetCurrentFolder(bool create)
+ private static async Task<T> LastOrDefaultAsync<T>(IAsyncEnumerable<T> src, Func<T, bool> predicate = null)
+ {
+ var res = default(T);
+ await foreach(var x in src)
+ if (predicate == null || predicate(x))
+ res = x;
+
+ return res;
+ }
+
+
+ private async Task GetCurrentFolderAsync(bool create, CancellationToken cancelToken)
{
var parentid = "0";
foreach(var p in m_path.Split(new string[] {"/"}, StringSplitOptions.RemoveEmptyEntries))
{
- var el = (MiniFolder)PagedFileListResponse(parentid, true).FirstOrDefault(x => x.Name == p);
+ var el = (MiniFolder)await FirstOrDefaultAsync(PagedFileListResponseAsync(parentid, true, cancelToken), x => x.Name == p);
if (el == null)
{
if (!create)
throw new FolderMissingException();
- el = m_oauth.PostAndGetJSONData<ListFolderResponse>(
+ el = await m_oauth.PostAndGetJSONDataAsync<ListFolderResponse>(
string.Format("{0}/folders", BOX_API_URL),
- new CreateItemRequest() { Name = p, Parent = new IDReference() { ID = parentid } }
+ new CreateItemRequest() { Name = p, Parent = new IDReference() { ID = parentid } },
+ null,
+ cancelToken
);
}
@@ -148,13 +166,13 @@ namespace Duplicati.Library.Backend.Box
m_currentfolder = parentid;
}
- private string GetFileID(string name)
+ private async Task<string> GetFileIDAsync(string name, CancellationToken cancelToken)
{
if (m_filecache.ContainsKey(name))
return m_filecache[name];
// Make sure we enumerate this, otherwise the m_filecache is empty.
- PagedFileListResponse(CurrentFolder, false).LastOrDefault();
+ await LastOrDefaultAsync(PagedFileListResponseAsync(await GetCurrentFolderAsync(cancelToken), false, cancelToken));
if (m_filecache.ContainsKey(name))
return m_filecache[name];
@@ -162,7 +180,7 @@ namespace Duplicati.Library.Backend.Box
throw new FileMissingException();
}
- private IEnumerable<FileEntity> PagedFileListResponse(string parentid, bool onlyfolders)
+ private async IAsyncEnumerable<FileEntity> PagedFileListResponseAsync(string parentid, bool onlyfolders, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
{
var offset = 0;
var done = false;
@@ -172,7 +190,7 @@ namespace Duplicati.Library.Backend.Box
do
{
- var resp = m_oauth.GetJSONData<ShortListResponse>(string.Format("{0}/folders/{1}/items?limit={2}&offset={3}&fields=name,size,modified_at", BOX_API_URL, parentid, PAGE_SIZE, offset));
+ var resp = await m_oauth.GetJSONDataAsync<ShortListResponse>(string.Format("{0}/folders/{1}/items?limit={2}&offset={3}&fields=name,size,modified_at", BOX_API_URL, parentid, PAGE_SIZE, offset), cancelToken);
if (resp.Entries == null || resp.Entries.Length == 0)
break;
@@ -208,12 +226,12 @@ namespace Duplicati.Library.Backend.Box
var createreq = new CreateItemRequest() {
Name = remotename,
Parent = new IDReference() {
- ID = CurrentFolder
+ ID = await GetCurrentFolderAsync(cancelToken)
}
};
if (m_filecache.Count == 0)
- PagedFileListResponse(CurrentFolder, false);
+ await LastOrDefaultAsync(PagedFileListResponseAsync(await GetCurrentFolderAsync(cancelToken), false, cancelToken));
var existing = m_filecache.ContainsKey(remotename);
@@ -242,47 +260,31 @@ namespace Duplicati.Library.Backend.Box
}
}
- public void Get(string remotename, System.IO.Stream stream)
+ public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
- using (var resp = m_oauth.GetResponse(string.Format("{0}/files/{1}/content", BOX_API_URL, GetFileID(remotename))))
- using(var rs = Duplicati.Library.Utility.AsyncHttpRequest.TrySetTimeout(resp.GetResponseStream()))
- Library.Utility.Utility.CopyStream(rs, stream);
+ using (var resp = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}/content", BOX_API_URL, await GetFileIDAsync(remotename, cancelToken)), null, null, cancelToken))
+ using (var rs = await resp.Content.ReadAsStreamAsync())
+ await Library.Utility.Utility.CopyStreamAsync(rs, stream, cancelToken);
}
#endregion
#region IBackend implementation
- public System.Collections.Generic.IEnumerable<IFileEntry> List()
- {
- return
- from n in PagedFileListResponse(CurrentFolder, false)
- select (IFileEntry)new FileEntry(n.Name, n.Size, n.ModifiedAt, n.ModifiedAt) { IsFolder = n.Type == "folder" };
- }
-
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
- {
- using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
- return PutAsync(remotename, fs, cancelToken);
- }
-
- public void Get(string remotename, string filename)
- {
- using (System.IO.FileStream fs = System.IO.File.Create(filename))
- Get(remotename, fs);
- }
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => this.CondensePaginatedListAsync(cancelToken);
- public void Delete(string remotename)
+ public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
- var fileid = GetFileID(remotename);
+ var fileid = await GetFileIDAsync(remotename, cancelToken);
try
{
- using(var r = m_oauth.GetResponse(string.Format("{0}/files/{1}", BOX_API_URL, fileid), null, "DELETE"))
+ using(var r = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}", BOX_API_URL, fileid), null, "DELETE", cancelToken))
{
}
if (m_deleteFromTrash)
- using(var r = m_oauth.GetResponse(string.Format("{0}/files/{1}/trash", BOX_API_URL, fileid), null, "DELETE"))
+ using(var r = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}/trash", BOX_API_URL, fileid), null, "DELETE", cancelToken))
{
}
}
@@ -293,14 +295,12 @@ namespace Duplicati.Library.Backend.Box
}
}
- public void Test()
- {
- this.TestList();
- }
+ public Task TestAsync(CancellationToken cancelToken)
+ => this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public async Task CreateFolderAsync(CancellationToken cancelToken)
{
- GetCurrentFolder(true);
+ await GetCurrentFolderAsync(true, cancelToken);
}
public string DisplayName
@@ -342,6 +342,8 @@ namespace Duplicati.Library.Backend.Box
get { return new string[] { new Uri(BOX_API_URL).Host, new Uri(BOX_UPLOAD_URL).Host }; }
}
+ public bool SupportsStreaming => true;
+
#endregion
#region IDisposable implementation
@@ -350,6 +352,12 @@ namespace Duplicati.Library.Backend.Box
{
}
+ public async IAsyncEnumerable<IFileEntry> ListEnumerableAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
+ {
+ await foreach(var p in PagedFileListResponseAsync(await GetCurrentFolderAsync(cancelToken), false, cancelToken))
+ yield return (IFileEntry)p;
+ }
+
#endregion
private class MiniUser : IDReference
diff --git a/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj b/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj
index 6ac3082d2..4b7e95088 100644
--- a/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj
+++ b/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>Box backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.Box</RootNamespace>
</PropertyGroup>
<ItemGroup>
@@ -17,7 +19,7 @@
<ProjectReference Include="..\..\Logging\Duplicati.Library.Logging.csproj" />
<ProjectReference Include="..\..\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj" />
</ItemGroup>
-
+
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
diff --git a/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs b/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs
index ece4510e7..4594a8d6d 100644
--- a/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs
+++ b/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs
@@ -1,4 +1,6 @@
#region Disclaimer / License
+using System.Net.Http.Headers;
+using System.Linq;
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
@@ -22,14 +24,15 @@ using Duplicati.Library.Interface;
using System;
using System.Collections.Generic;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-namespace Duplicati.Library.Backend
+namespace Duplicati.Library.Backend.CloudFiles
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
- public class CloudFiles : IBackend, IStreamingBackend
+ public class CloudFiles : IBackend, IBackendPagination
{
public const string AUTH_URL_US = "https://identity.api.rackspacecloud.com/auth";
public const string AUTH_URL_UK = "https://lon.auth.api.rackspacecloud.com/v1.0";
@@ -43,6 +46,12 @@ namespace Duplicati.Library.Backend
private string m_storageUrl = null;
private string m_authToken = null;
private readonly string m_authUrl;
+
+ private readonly HttpClient m_client = new HttpClient(
+ new HttpClientHandler() {
+ PreAuthenticate = true
+ }
+ );
private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE];
@@ -130,7 +139,10 @@ namespace Duplicati.Library.Backend
get { return "cloudfiles"; }
}
- public IEnumerable<IFileEntry> List()
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => this.CondensePaginatedListAsync(cancelToken);
+
+ public async IAsyncEnumerable<IFileEntry> ListEnumerableAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
{
string extraUrl = "?format=xml&limit=" + ITEM_LIST_LIMIT.ToString();
string markerUrl = "";
@@ -141,13 +153,12 @@ namespace Duplicati.Library.Backend
{
var doc = new System.Xml.XmlDocument();
- var req = CreateRequest("", extraUrl + markerUrl);
+ var req = await CreateRequestAsync("", extraUrl + markerUrl, cancelToken);
try
{
- var areq = new Utility.AsyncHttpRequest(req);
- using (var resp = (HttpWebResponse)areq.GetResponse())
- using (var s = areq.GetResponseStream())
+ using (var resp = await m_client.SendAsync(req, cancelToken))
+ using (var s = await resp.Content.ReadAsStreamAsync())
doc.Load(s);
}
catch (WebException wex)
@@ -166,7 +177,7 @@ namespace Duplicati.Library.Backend
//The response should be 404 from the server, but it is not :(
if (lst.Count == 0 && markerUrl == "") //Only on first iteration
{
- try { CreateFolder(); }
+ try { await CreateFolderAsync(cancelToken); }
catch { } //Ignore
}
@@ -194,35 +205,22 @@ namespace Duplicati.Library.Backend
} while (repeat);
}
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
- {
- using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
- return PutAsync(remotename, fs, cancelToken);
- }
-
- public void Get(string remotename, string filename)
- {
- using (System.IO.FileStream fs = System.IO.File.Create(filename))
- Get(remotename, fs);
- }
-
- public void Delete(string remotename)
+ public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
try
{
- HttpWebRequest req = CreateRequest("/" + remotename, "");
+ var req = await CreateRequestAsync("/" + remotename, "", cancelToken);
- req.Method = "DELETE";
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
- using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
+ req.Method = HttpMethod.Delete;
+ using (var resp = await m_client.SendAsync(req, cancelToken))
{
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new FileMissingException();
if ((int)resp.StatusCode >= 300)
- throw new WebException(Strings.CloudFiles.FileDeleteError, null, WebExceptionStatus.ProtocolError, resp);
+ throw new WebException(Strings.CloudFiles.FileDeleteError, WebExceptionStatus.ProtocolError);
else
- using (areq.GetResponseStream())
+ using (await resp.Content.ReadAsStreamAsync())
{ }
}
}
@@ -259,18 +257,15 @@ namespace Duplicati.Library.Backend
#region IBackend_v2 Members
- public void Test()
- {
+ public Task TestAsync(CancellationToken cancelToken)
//The "Folder not found" is not detectable :(
- this.TestList();
- }
+ => this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public async Task CreateFolderAsync(CancellationToken cancelToken)
{
- HttpWebRequest createReq = CreateRequest("", "");
- createReq.Method = "PUT";
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(createReq);
- using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
+ var createReq = await CreateRequestAsync("", "", cancelToken);
+ createReq.Method = HttpMethod.Put;
+ using (var resp = await m_client.SendAsync(createReq, cancelToken))
{ }
}
@@ -291,18 +286,19 @@ namespace Duplicati.Library.Backend
get { return new string[] { new Uri(m_authUrl).Host, string.IsNullOrWhiteSpace(m_storageUrl) ? null : new Uri(m_storageUrl).Host }; }
}
- public void Get(string remotename, System.IO.Stream stream)
+ public bool SupportsStreaming => true;
+
+ public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
- var req = CreateRequest("/" + remotename, "");
- req.Method = "GET";
+ var req = await CreateRequestAsync("/" + remotename, "", cancelToken);
+ req.Method = HttpMethod.Get;
- var areq = new Utility.AsyncHttpRequest(req);
- using (var resp = areq.GetResponse())
- using (var s = areq.GetResponseStream())
+ using (var resp = await m_client.SendAsync(req, cancelToken))
+ using (var s = await resp.Content.ReadAsStreamAsync())
using (var mds = new Utility.MD5CalculatingStream(s))
{
- string md5Hash = resp.Headers["ETag"];
- Utility.Utility.CopyStream(mds, stream, true, m_copybuffer);
+ string md5Hash = resp.Headers.GetValues("ETag").FirstOrDefault();
+ await Utility.Utility.CopyStreamAsync(mds, stream, true, cancelToken, m_copybuffer);
if (!String.Equals(mds.GetFinalHashString(), md5Hash, StringComparison.OrdinalIgnoreCase))
throw new Exception(Strings.CloudFiles.ETagVerificationError);
@@ -311,62 +307,30 @@ namespace Duplicati.Library.Backend
public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
- HttpWebRequest req = CreateRequest("/" + remotename, "");
- req.Method = "PUT";
- req.ContentType = "application/octet-stream";
-
- try { req.ContentLength = stream.Length; }
- catch { }
-
- //If we can pre-calculate the MD5 hash before transmission, do so
- /*if (stream.CanSeek)
+ var req = await CreateRequestAsync("/" + remotename, "", cancelToken);
+ req.Method = HttpMethod.Put;
+ using (var mds = new Utility.MD5CalculatingStream(stream))
+ using (var body = new StreamContent(mds))
{
- System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
- req.Headers["ETag"] = Core.Utility.ByteArrayAsHexString(md5.ComputeHash(stream)).ToLower(System.Globalization.CultureInfo.InvariantCulture);
- stream.Seek(0, System.IO.SeekOrigin.Begin);
-
- using (System.IO.Stream s = req.GetRequestStream())
- Core.Utility.CopyStream(stream, s);
-
- //Reset the timeout to the default value of 100 seconds to
- // avoid blocking the GetResponse() call
- req.Timeout = 100000;
+ body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
- //The server handles the eTag verification for us, and gives an error if the hash was a mismatch
- using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
- if ((int)resp.StatusCode >= 300)
- throw new WebException(Strings.CloudFiles.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
-
- }
- else //Otherwise use a client-side calculation
- */
- //TODO: We cannot use the local MD5 calculation, because that could involve a throttled read,
- // and may invoke various events
- {
- string fileHash = null;
-
- long streamLen = -1;
- try { streamLen = stream.Length; }
+ try { body.Headers.ContentLength = stream.Length; }
catch { }
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
- using (System.IO.Stream s = areq.GetRequestStream(streamLen))
- using (var mds = new Utility.MD5CalculatingStream(s))
- {
- await Utility.Utility.CopyStreamAsync(stream, mds, tryRewindSource: true, cancelToken: cancelToken);
- fileHash = mds.GetFinalHashString();
- }
+ req.Content = body;
+ //TODO: We cannot use the local MD5 calculation, because that could involve a throttled read,
+ // and may invoke various events
string md5Hash = null;
//We need to verify the eTag locally
try
{
- using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
+ using (var resp = await m_client.SendAsync(req))
if ((int)resp.StatusCode >= 300)
- throw new WebException(Strings.CloudFiles.FileUploadError, null, WebExceptionStatus.ProtocolError, resp);
+ throw new WebException(Strings.CloudFiles.FileUploadError, WebExceptionStatus.ProtocolError);
else
- md5Hash = resp.Headers["ETag"];
+ md5Hash = resp.Headers.GetValues("ETag").FirstOrDefault();
}
catch (WebException wex)
{
@@ -378,11 +342,11 @@ namespace Duplicati.Library.Backend
throw;
}
-
+ var fileHash = mds.GetFinalHashString();
if (md5Hash == null || !String.Equals(md5Hash, fileHash, StringComparison.OrdinalIgnoreCase))
{
//Remove the broken file
- try { Delete(remotename); }
+ try { await DeleteAsync(remotename, cancelToken); }
catch { }
throw new Exception(Strings.CloudFiles.ETagVerificationError);
@@ -392,34 +356,32 @@ namespace Duplicati.Library.Backend
#endregion
- private HttpWebRequest CreateRequest(string remotename, string query)
+ private async Task<HttpRequestMessage> CreateRequestAsync(string remotename, string query, CancellationToken cancelToken)
{
//If this is the first call, get an authentication token
if (string.IsNullOrEmpty(m_authToken) || string.IsNullOrEmpty(m_storageUrl))
{
- HttpWebRequest authReq = (HttpWebRequest)HttpWebRequest.Create(m_authUrl);
+ var authReq = new HttpRequestMessage(HttpMethod.Get, m_authUrl);
authReq.Headers.Add("X-Auth-User", m_username);
authReq.Headers.Add("X-Auth-Key", m_password);
- authReq.Method = "GET";
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(authReq);
- using (WebResponse resp = areq.GetResponse())
+ using (var resp = await m_client.SendAsync(authReq, cancelToken))
{
- m_storageUrl = resp.Headers["X-Storage-Url"];
- m_authToken = resp.Headers["X-Auth-Token"];
+ m_storageUrl = resp.Headers.GetValues("X-Storage-Url").FirstOrDefault();
+ m_authToken = resp.Headers.GetValues("X-Auth-Token").FirstOrDefault();
}
if (string.IsNullOrEmpty(m_authToken) || string.IsNullOrEmpty(m_storageUrl))
throw new Exception(Strings.CloudFiles.UnexpectedResponseError);
}
- HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(m_storageUrl + UrlEncode(m_path + remotename) + query);
+ var req = new HttpRequestMessage(HttpMethod.Get, m_storageUrl + UrlEncode(m_path + remotename) + query);
req.Headers.Add("X-Auth-Token", UrlEncode(m_authToken));
- req.UserAgent = "Duplicati CloudFiles Backend v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
- req.KeepAlive = false;
- req.PreAuthenticate = true;
- req.AllowWriteStreamBuffering = false;
+ req.Headers.UserAgent.ParseAdd("Duplicati CloudFiles Backend v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
+ req.Headers.ConnectionClose = true;
+ // TODO-DNC: Not supported, but no longer required?
+ //req.Headers.AllowWriteStreamBuffering = false;
return req;
}
diff --git a/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj b/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj
index 35b4d9b0d..454016edc 100644
--- a/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj
+++ b/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>CloudFiles backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.CloudFiles</RootNamespace>
</PropertyGroup>
<ItemGroup>
diff --git a/Duplicati/Library/Backend/CloudFiles/Strings.cs b/Duplicati/Library/Backend/CloudFiles/Strings.cs
index b6f0a8866..64b562527 100644
--- a/Duplicati/Library/Backend/CloudFiles/Strings.cs
+++ b/Duplicati/Library/Backend/CloudFiles/Strings.cs
@@ -1,5 +1,5 @@
using Duplicati.Library.Localization.Short;
-namespace Duplicati.Library.Backend.Strings {
+namespace Duplicati.Library.Backend.CloudFiles.Strings {
internal static class CloudFiles {
public static string DescriptionAuthenticationURLLong_v2(string optionname) { return LC.L(@"CloudFiles use different servers for authentication based on where the account resides, use this option to set an alternate authentication URL. This option overrides --{0}.", optionname); }
public static string DescriptionAuthenticationURLShort { get { return LC.L(@"Provide another authentication URL"); } }
diff --git a/Duplicati/Library/Backend/Dropbox/Dropbox.cs b/Duplicati/Library/Backend/Dropbox/Dropbox.cs
index 835deb38b..d93b87f30 100644
--- a/Duplicati/Library/Backend/Dropbox/Dropbox.cs
+++ b/Duplicati/Library/Backend/Dropbox/Dropbox.cs
@@ -6,11 +6,11 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
-namespace Duplicati.Library.Backend
+namespace Duplicati.Library.Backend.Dropbox
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
- public class Dropbox : IBackend, IStreamingBackend
+ public class Dropbox : IBackend, IBackendPagination
{
private const string AUTHID_OPTION = "authid";
@@ -77,11 +77,11 @@ namespace Duplicati.Library.Backend
return ife;
}
- private T HandleListExceptions<T>(Func<T> func)
+ private async Task<T> HandleListExceptionsAsync<T>(Func<Task<T>> func)
{
try
{
- return func();
+ return await func();
}
catch (DropboxException de)
{
@@ -92,39 +92,30 @@ namespace Duplicati.Library.Backend
}
}
- public IEnumerable<IFileEntry> List()
+ public async IAsyncEnumerable<IFileEntry> ListEnumerableAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
{
- var lfr = HandleListExceptions(() => dbx.ListFiles(m_path));
+ var lfr = await HandleListExceptionsAsync(() => dbx.ListFilesAsync(m_path, cancelToken));
foreach (var md in lfr.entries)
yield return ParseEntry(md);
while (lfr.has_more)
{
- lfr = HandleListExceptions(() => dbx.ListFilesContinue(lfr.cursor));
+ lfr = await HandleListExceptionsAsync(() => dbx.ListFilesContinueAsync(lfr.cursor, cancelToken));
foreach (var md in lfr.entries)
yield return ParseEntry(md);
}
}
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
- {
- using(FileStream fs = File.OpenRead(filename))
- return PutAsync(remotename, fs, cancelToken);
- }
-
- public void Get(string remotename, string filename)
- {
- using(FileStream fs = File.Create(filename))
- Get(remotename, fs);
- }
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => this.CondensePaginatedListAsync(cancelToken);
- public void Delete(string remotename)
+ public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
try
{
string path = String.Format("{0}/{1}", m_path, remotename);
- dbx.Delete(path);
+ await dbx.DeleteAsync(path, cancelToken);
}
catch (DropboxException)
{
@@ -150,16 +141,16 @@ namespace Duplicati.Library.Backend
get { return WebApi.Dropbox.Hosts(); }
}
- public void Test()
- {
- this.TestList();
- }
+ public bool SupportsStreaming => true;
+
+ public Task TestAsync(CancellationToken cancelToken)
+ => this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public async Task CreateFolderAsync(CancellationToken cancelToken)
{
try
{
- dbx.CreateFolder(m_path);
+ await dbx.CreateFolderAsync(m_path, cancelToken);
}
catch (DropboxException de)
{
@@ -184,12 +175,12 @@ namespace Duplicati.Library.Backend
}
}
- public void Get(string remotename, Stream stream)
+ public async Task GetAsync(string remotename, Stream stream, CancellationToken cancelToken)
{
try
{
string path = string.Format("{0}/{1}", m_path, remotename);
- dbx.DownloadFile(path, stream);
+ await dbx.DownloadFileAsync(path, stream, cancelToken);
}
catch (DropboxException)
{
diff --git a/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs b/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs
index b277f6312..4c60c8c9c 100644
--- a/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs
+++ b/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs
@@ -1,4 +1,7 @@
-using Duplicati.Library.Utility;
+using System.Net.Http.Headers;
+using System.Net.Http;
+using System.Security.AccessControl;
+using Duplicati.Library.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -7,7 +10,7 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
-namespace Duplicati.Library.Backend
+namespace Duplicati.Library.Backend.Dropbox
{
public class DropboxHelper : OAuthHelper
{
@@ -21,7 +24,7 @@ namespace Duplicati.Library.Backend
base.AccessTokenOnly = true;
}
- public ListFolderResult ListFiles(string path)
+ public async Task<ListFolderResult> ListFilesAsync(string path, CancellationToken cancelToken)
{
var pa = new PathArg
{
@@ -30,41 +33,41 @@ namespace Duplicati.Library.Backend
try
{
- return PostAndGetJSONData<ListFolderResult>(WebApi.Dropbox.ListFilesUrl(), pa);
+ return await PostAndGetJSONDataAsync<ListFolderResult>(WebApi.Dropbox.ListFilesUrl(), pa, null, cancelToken);
}
catch (Exception ex)
{
- HandleDropboxException(ex, false);
+ await HandleDropboxExceptionAsync(ex, false, cancelToken);
throw;
}
}
- public ListFolderResult ListFilesContinue(string cursor)
+ public async Task<ListFolderResult> ListFilesContinueAsync(string cursor, CancellationToken cancelToken)
{
var lfca = new ListFolderContinueArg() { cursor = cursor };
try
{
- return PostAndGetJSONData<ListFolderResult>(WebApi.Dropbox.ListFilesContinueUrl(), lfca);
+ return await PostAndGetJSONDataAsync<ListFolderResult>(WebApi.Dropbox.ListFilesContinueUrl(), lfca, null, cancelToken);
}
catch (Exception ex)
{
- HandleDropboxException(ex, false);
+ await HandleDropboxExceptionAsync(ex, false, cancelToken);
throw;
}
}
- public FolderMetadata CreateFolder(string path)
+ public async Task<FolderMetadata> CreateFolderAsync(string path, CancellationToken cancelToken)
{
var pa = new PathArg() { path = path };
try
{
- return PostAndGetJSONData<FolderMetadata>(WebApi.Dropbox.CreateFolderUrl(), pa);
+ return await PostAndGetJSONDataAsync<FolderMetadata>(WebApi.Dropbox.CreateFolderUrl(), pa, null, cancelToken);
}
catch (Exception ex)
{
- HandleDropboxException(ex, false);
+ await HandleDropboxExceptionAsync(ex, false, cancelToken);
throw;
}
}
@@ -75,58 +78,57 @@ namespace Duplicati.Library.Backend
var ussa = new UploadSessionStartArg();
var chunksize = (int)Math.Min(DROPBOX_MAX_CHUNK_UPLOAD, stream.Length);
+ long globalBytesRead = 0;
- var req = CreateRequest(WebApi.Dropbox.UploadSessionStartUrl(), "POST");
- req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(ussa);
- req.ContentType = "application/octet-stream";
- req.ContentLength = chunksize;
- req.Timeout = 200000;
+ var req = await CreateRequestAsync(WebApi.Dropbox.UploadSessionStartUrl(), "POST", cancelToken);
+ req.Headers.Add(API_ARG_HEADER, JsonConvert.SerializeObject(ussa));
- var areq = new AsyncHttpRequest(req);
+ var body = new StreamContent(new PartialStream(stream, globalBytesRead, chunksize));
+ body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
+ body.Headers.ContentLength = chunksize;
+
+ req.Content = body;
+
+ var tcs = new CancellationTokenSource(200000);
+ if (cancelToken.CanBeCanceled)
+ cancelToken.Register(() => tcs.Cancel());
byte[] buffer = new byte[Utility.Utility.DEFAULT_BUFFER_SIZE];
int sizeToRead = Math.Min((int)Utility.Utility.DEFAULT_BUFFER_SIZE, chunksize);
- ulong globalBytesRead = 0;
- using (var rs = areq.GetRequestStream())
- {
- int bytesRead = 0;
- do
- {
- bytesRead = await stream.ReadAsync(buffer, 0, sizeToRead, cancelToken).ConfigureAwait(false);
- globalBytesRead += (ulong)bytesRead;
- await rs.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
- }
- while (bytesRead > 0 && globalBytesRead < (ulong)chunksize);
- }
-
- var ussr = await ReadJSONResponseAsync<UploadSessionStartResult>(areq, cancelToken); // pun intended
+ var ussr = await ReadJSONResponseAsync<UploadSessionStartResult>(req, tcs.Token); // pun intended
+ globalBytesRead += chunksize;
// keep appending until finished
// 1) read into buffer
- while (globalBytesRead < (ulong)stream.Length)
+ while (globalBytesRead < stream.Length)
{
- var remaining = (ulong)stream.Length - globalBytesRead;
+ var remaining = stream.Length - globalBytesRead;
// start an append request
var usaa = new UploadSessionAppendArg();
usaa.cursor.session_id = ussr.session_id;
- usaa.cursor.offset = globalBytesRead;
+ usaa.cursor.offset = (ulong)globalBytesRead;
usaa.close = remaining < DROPBOX_MAX_CHUNK_UPLOAD;
chunksize = (int)Math.Min(DROPBOX_MAX_CHUNK_UPLOAD, (long)remaining);
- req = CreateRequest(WebApi.Dropbox.UploadSessionAppendUrl(), "POST");
- req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(usaa);
- req.ContentType = "application/octet-stream";
- req.ContentLength = chunksize;
- req.Timeout = 200000;
+ req = await CreateRequestAsync(WebApi.Dropbox.UploadSessionAppendUrl(), "POST", cancelToken);
+ req.Headers.Add(API_ARG_HEADER, JsonConvert.SerializeObject(usaa));
+
+ body = new StreamContent(new PartialStream(stream, globalBytesRead, chunksize));
+ body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
+ body.Headers.ContentLength = chunksize;
- areq = new AsyncHttpRequest(req);
+ req.Content = body;
+
+ tcs = new CancellationTokenSource(200000);
+ if (cancelToken.CanBeCanceled)
+ cancelToken.Register(() => tcs.Cancel());
int bytesReadInRequest = 0;
sizeToRead = Math.Min(chunksize, (int)Utility.Utility.DEFAULT_BUFFER_SIZE);
- using (var rs = areq.GetRequestStream())
+ using (var rs = req.GetRequestStream())
{
int bytesRead = 0;
do
@@ -140,9 +142,10 @@ namespace Duplicati.Library.Backend
while (bytesRead > 0 && bytesReadInRequest < chunksize);
}
- using (var response = GetResponse(areq))
- using (var sr = new StreamReader(response.GetResponseStream()))
- await sr.ReadToEndAsync().ConfigureAwait(false);
+ using (var response = await GetResponseAsync(req, null, tcs.Token))
+ await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ globalBytesRead += (uint)chunksize;
}
// finish session and commit
@@ -150,84 +153,86 @@ namespace Duplicati.Library.Backend
{
var usfa = new UploadSessionFinishArg();
usfa.cursor.session_id = ussr.session_id;
- usfa.cursor.offset = globalBytesRead;
+ usfa.cursor.offset = (ulong)globalBytesRead;
usfa.commit.path = path;
- req = CreateRequest(WebApi.Dropbox.UploadSessionFinishUrl(), "POST");
- req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(usfa);
- req.ContentType = "application/octet-stream";
- req.Timeout = 200000;
+ req = await CreateRequestAsync(WebApi.Dropbox.UploadSessionFinishUrl(), "POST", cancelToken);
+ req.Headers.Add(API_ARG_HEADER, JsonConvert.SerializeObject(usfa));
+ //req.ContentType = "application/octet-stream";
- return ReadJSONResponse<FileMetaData>(req);
+ tcs = new CancellationTokenSource(200000);
+ if (cancelToken.CanBeCanceled)
+ cancelToken.Register(() => tcs.Cancel());
+
+ return await ReadJSONResponseAsync<FileMetaData>(req, tcs.Token);
}
catch (Exception ex)
{
- HandleDropboxException(ex, true);
+ await HandleDropboxExceptionAsync(ex, true, cancelToken);
throw;
}
}
- public void DownloadFile(string path, Stream fs)
+ public async Task DownloadFileAsync(string path, Stream fs, CancellationToken cancelToken)
{
try
{
var pa = new PathArg { path = path };
- var req = CreateRequest(WebApi.Dropbox.DownloadFilesUrl(), "POST");
- req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(pa);
+ var req = await CreateRequestAsync(WebApi.Dropbox.DownloadFilesUrl(), "POST", cancelToken);
+ req.Headers.Add(API_ARG_HEADER, JsonConvert.SerializeObject(pa));
- using (var response = GetResponse(req))
- Utility.Utility.CopyStream(response.GetResponseStream(), fs);
+ using (var response = await GetResponseAsync(req, null, cancelToken))
+ using (var rs = await response.Content.ReadAsStreamAsync())
+ await Utility.Utility.CopyStreamAsync(rs, fs, cancelToken);
}
catch (Exception ex)
{
- HandleDropboxException(ex, true);
+ await HandleDropboxExceptionAsync(ex, true, cancelToken);
throw;
}
}
- public void Delete(string path)
+ public async Task DeleteAsync(string path, CancellationToken cancelToken)
{
try
{
var pa = new PathArg() { path = path };
- using (var response = GetResponse(WebApi.Dropbox.DeleteUrl(), pa))
- using(var sr = new StreamReader(response.GetResponseStream()))
- sr.ReadToEnd();
+ using (var response = await GetResponseAsync(WebApi.Dropbox.DeleteUrl(), pa, null, cancelToken))
+ await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
- HandleDropboxException(ex, true);
+ await HandleDropboxExceptionAsync(ex, true, cancelToken);
throw;
}
}
- private void HandleDropboxException(Exception ex, bool filerequest)
+ private async Task HandleDropboxExceptionAsync(Exception ex, bool filerequest, CancellationToken cancelToken)
{
- if (ex is WebException exception)
+ if (ex is HttpRequestStatusException exception)
{
string json = string.Empty;
try
{
- using (var sr = new StreamReader(exception.Response.GetResponseStream()))
- json = sr.ReadToEnd();
+ json = await exception.Response.Content.ReadAsStringAsync();
}
catch { }
// Special mapping for exceptions:
// https://www.dropbox.com/developers-v1/core/docs
- if (exception.Response is HttpWebResponse httpResp)
+ if (exception.Response != null)
{
- if (httpResp.StatusCode == HttpStatusCode.NotFound)
+ if (exception.Response.StatusCode == HttpStatusCode.NotFound)
{
if (filerequest)
throw new Duplicati.Library.Interface.FileMissingException(json);
else
throw new Duplicati.Library.Interface.FolderMissingException(json);
}
- if (httpResp.StatusCode == HttpStatusCode.Conflict)
+ if (exception.Response.StatusCode == HttpStatusCode.Conflict)
{
//TODO: Should actually parse and see if something else happens
if (filerequest)
@@ -235,9 +240,9 @@ namespace Duplicati.Library.Backend
else
throw new Duplicati.Library.Interface.FolderMissingException(json);
}
- if (httpResp.StatusCode == HttpStatusCode.Unauthorized)
+ if (exception.Response.StatusCode == HttpStatusCode.Unauthorized)
ThrowAuthException(json, exception);
- if ((int)httpResp.StatusCode == 429 || (int)httpResp.StatusCode == 507)
+ if ((int)exception.Response.StatusCode == 429 || (int)exception.Response.StatusCode == 507)
ThrowOverQuotaError();
}
@@ -259,6 +264,63 @@ namespace Duplicati.Library.Backend
}
}
+ public class PartialStream : Stream
+ {
+ private readonly Stream m_source;
+ private readonly long m_offset;
+ private readonly long m_length;
+ private long m_position;
+
+ public PartialStream(Stream source, long offset, long length)
+ {
+ m_source = source ?? throw new ArgumentNullException(nameof(source));
+ m_offset = offset;
+ m_length = length;
+ }
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override long Length => m_length;
+ public override long Position { get => m_position; set => throw new NotSupportedException(); }
+
+ public override void Flush()
+ {
+ }
+
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ var c = (int)Math.Min(count, m_length - m_position);
+ var r = await m_source.ReadAsync(buffer, offset, c, cancellationToken);
+ m_position += r;
+ return r;
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var c = (int)Math.Min(count, m_length - m_position);
+ var r = m_source.Read(buffer, offset, c);
+ m_position += r;
+ return r;
+ }
+
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
public class DropboxException : Exception
{
public JObject errorJSON { get; set; }
diff --git a/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj b/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj
index 5ed63c06a..033e97407 100644
--- a/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj
+++ b/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>Dropbox backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.Dropbox</RootNamespace>
</PropertyGroup>
<ItemGroup>
diff --git a/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj b/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj
index e2af3070a..5e05a5f71 100644
--- a/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj
+++ b/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>A FTP backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.FTP</RootNamespace>
</PropertyGroup>
<ItemGroup>
diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs
index e5c2bbc2e..68f2d052d 100644
--- a/Duplicati/Library/Backend/FTP/FTPBackend.cs
+++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs
@@ -30,7 +30,7 @@ namespace Duplicati.Library.Backend
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
- public class FTP : IBackend, IStreamingBackend
+ public class FTP : IBackend
{
private System.Net.NetworkCredential m_userInfo;
private readonly string m_url;
@@ -167,19 +167,28 @@ namespace Duplicati.Library.Backend
get { return "ftp"; }
}
- private T HandleListExceptions<T>(Func<T> func, System.Net.FtpWebRequest req)
+ private async Task<T> HandleListExceptionsAsync<T>(Func<T> func, System.Net.FtpWebRequest req)
{
T ret = default(T);
- Action action = () => ret = func();
- HandleListExceptions(action, req);
+ Func<Task> action = () => Task.FromResult(ret = func());
+ await HandleListExceptionsAsync(action, req);
return ret;
}
- private void HandleListExceptions(Action action, System.Net.FtpWebRequest req)
+
+ private async Task<T> HandleListExceptionsAsync<T>(Func<Task<T>> func, System.Net.FtpWebRequest req)
+ {
+ T ret = default(T);
+ Func<Task> action = async () => ret = await func();
+ await HandleListExceptionsAsync(action, req);
+ return ret;
+ }
+
+ private async Task HandleListExceptionsAsync(Func<Task> action, System.Net.FtpWebRequest req)
{
try
{
- action();
+ await action();
}
catch (System.Net.WebException wex)
{
@@ -190,12 +199,10 @@ namespace Duplicati.Library.Backend
}
}
- public IEnumerable<IFileEntry> List()
- {
- return List("");
- }
+ public IAsyncEnumerable<IFileEntry> ListEnumerableAsync(CancellationToken cancelToken)
+ => ListEnumerableAsync("", cancelToken);
- public IEnumerable<IFileEntry> List(string filename)
+ public async IAsyncEnumerable<IFileEntry> ListEnumerableAsync(string filename, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
{
var req = CreateRequest(filename);
req.Method = System.Net.WebRequestMethods.Ftp.ListDirectoryDetails;
@@ -207,18 +214,17 @@ namespace Duplicati.Library.Backend
try
{
- HandleListExceptions(
- () =>
+ await HandleListExceptionsAsync(
+ async () =>
{
- var areq = new Utility.AsyncHttpRequest(req);
- resp = areq.GetResponse();
- rs = areq.GetResponseStream();
+ resp = await req.GetResponseAsync();
+ rs = resp.GetResponseStream();
sr = new System.IO.StreamReader(new StreamReadHelper(rs));
},
req);
string line;
- while ((line = HandleListExceptions(sr.ReadLine, req)) != null)
+ while ((line = await HandleListExceptionsAsync(sr.ReadLine, req)) != null)
{
FileEntry f = ParseLine(line);
if (f != null)
@@ -267,14 +273,14 @@ namespace Duplicati.Library.Backend
try { streamLen = input.Length; }
catch {}
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
- using (System.IO.Stream rs = areq.GetRequestStream(streamLen))
+ using (System.IO.Stream rs = await req.GetRequestStreamAsync())
await Utility.Utility.CopyStreamAsync(input, rs, true, cancelToken, m_copybuffer).ConfigureAwait(false);
if (m_listVerify)
{
- IEnumerable<IFileEntry> files = List(remotename);
- foreach(IFileEntry fe in files)
+ var filenames = new List<string>();
+ await foreach(IFileEntry fe in ListEnumerableAsync(cancelToken))
+ {
if (fe.Name.Equals(remotename) || fe.Name.EndsWith("/" + remotename, StringComparison.Ordinal) || fe.Name.EndsWith("\\" + remotename, StringComparison.Ordinal))
{
if (fe.Size < 0 || streamLen < 0 || fe.Size == streamLen)
@@ -282,8 +288,10 @@ namespace Duplicati.Library.Backend
throw new Exception(Strings.FTPBackend.ListVerifySizeFailure(remotename, fe.Size, streamLen));
}
+ filenames.Add(fe.Name);
+ }
- throw new Exception(Strings.FTPBackend.ListVerifyFailure(remotename, files.Select(n => n.Name)));
+ throw new Exception(Strings.FTPBackend.ListVerifyFailure(remotename, filenames));
}
}
@@ -296,36 +304,22 @@ namespace Duplicati.Library.Backend
}
}
- public Task PutAsync(string remotename, string localname, CancellationToken cancelToken)
- {
- using (System.IO.FileStream fs = System.IO.File.Open(localname, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read))
- return PutAsync(remotename, fs, cancelToken);
- }
-
- public void Get(string remotename, System.IO.Stream output)
+ public async Task GetAsync(string remotename, System.IO.Stream output, CancellationToken cancelToken)
{
var req = CreateRequest(remotename);
req.Method = System.Net.WebRequestMethods.Ftp.DownloadFile;
req.UseBinary = true;
- var areq = new Utility.AsyncHttpRequest(req);
- using (var resp = areq.GetResponse())
- using (var rs = areq.GetResponseStream())
- Utility.Utility.CopyStream(rs, output, false, m_copybuffer);
+ using (var resp = await req.GetResponseAsync())
+ using (var rs = resp.GetResponseStream())
+ await Utility.Utility.CopyStreamAsync(rs, output, false, cancelToken, m_copybuffer);
}
- public void Get(string remotename, string localname)
- {
- using (System.IO.FileStream fs = System.IO.File.Open(localname, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
- Get(remotename, fs);
- }
-
- public void Delete(string remotename)
+ public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
System.Net.FtpWebRequest req = CreateRequest(remotename);
req.Method = System.Net.WebRequestMethods.Ftp.DeleteFile;
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
- using (areq.GetResponse())
+ using (await req.GetResponseAsync())
{ }
}
@@ -357,18 +351,17 @@ namespace Duplicati.Library.Backend
get { return new string[] { new Uri(m_url).Host }; }
}
- public void Test()
- {
- this.TestList();
- }
+ public bool SupportsStreaming => true;
+
+ public Task TestAsync(CancellationToken cancelToken)
+ => this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public async Task CreateFolder()
{
System.Net.FtpWebRequest req = CreateRequest("", true);
req.Method = System.Net.WebRequestMethods.Ftp.MakeDirectory;
req.KeepAlive = false;
- Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
- using (areq.GetResponse())
+ using (await req.GetResponseAsync())
{ }
}
@@ -410,6 +403,16 @@ namespace Duplicati.Library.Backend
return req;
}
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task CreateFolderAsync(CancellationToken cancelToken)
+ {
+ throw new NotImplementedException();
+ }
+
/// <summary>
/// Private helper class to fix a bug with the StreamReader
/// </summary>
diff --git a/Duplicati/Library/Backend/FTP/Strings.cs b/Duplicati/Library/Backend/FTP/Strings.cs
index c018aec20..89f69f1fb 100644
--- a/Duplicati/Library/Backend/FTP/Strings.cs
+++ b/Duplicati/Library/Backend/FTP/Strings.cs
@@ -2,7 +2,7 @@ using System;
using Duplicati.Library.Localization.Short;
using System.Collections.Generic;
-namespace Duplicati.Library.Backend.Strings {
+namespace Duplicati.Library.Backend.FTP.Strings {
internal static class FTPBackend {
public static string Description { get { return LC.L(@"This backend can read and write data to an FTP based backend. Allowed formats are ""ftp://hostname/folder"" or ""ftp://username:password@hostname/folder"""); } }
public static string DescriptionFTPActiveLong { get { return LC.L(@"If this flag is set, the FTP connection is made in active mode. Even if the ""ftp-passive"" flag is also set, the connection will be made in active mode"); } }
diff --git a/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj b/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj
index 06d0927ff..1a5fedde9 100644
--- a/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj
+++ b/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>File backend for Duplicati</Description>
+ <RootNamespace>Duplicati.Library.Backend.File</RootNamespace>
</PropertyGroup>
<ItemGroup>
diff --git a/Duplicati/Library/Backend/File/FileBackend.cs b/Duplicati/Library/Backend/File/FileBackend.cs
index 1dd9752cd..7a48e9ec0 100644
--- a/Duplicati/Library/Backend/File/FileBackend.cs
+++ b/Duplicati/Library/Backend/File/FileBackend.cs
@@ -26,11 +26,11 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-namespace Duplicati.Library.Backend
+namespace Duplicati.Library.Backend.File
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
- public class File : IBackend, IStreamingBackend, IQuotaEnabledBackend, IRenameEnabledBackend
+ public class File : IBackend, IBackendPagination, IQuotaEnabledBackend, IRenameEnabledBackend
{
private const string OPTION_DESTINATION_MARKER = "alternate-destination-marker";
private const string OPTION_ALTERNATE_PATHS = "alternate-target-paths";
@@ -165,10 +165,13 @@ namespace Duplicati.Library.Backend
get { return "file"; }
}
- public IEnumerable<IFileEntry> List()
+ public async IAsyncEnumerable<IFileEntry> ListEnumerableAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancelToken)
{
PreAuthenticate();
+ // Remove warning about no awaits in method
+ await Task.FromResult(true);
+
if (!systemIO.DirectoryExists(m_path))
throw new FolderMissingException(Strings.FileBackend.FolderMissingError(m_path));
@@ -183,9 +186,12 @@ namespace Duplicati.Library.Backend
}
}
+ public Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
+ => this.CondensePaginatedListAsync(cancelToken);
+
#if DEBUG_RETRY
private static Random random = new Random();
- public async Task Put(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
+ public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
using(System.IO.FileStream writestream = systemIO.FileOpenWrite(GetRemoteName(remotename)))
{
@@ -197,42 +203,57 @@ namespace Duplicati.Library.Backend
#else
public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
- using (System.IO.FileStream writestream = systemIO.FileOpenWrite(GetRemoteName(remotename)))
- await Utility.Utility.CopyStreamAsync(stream, writestream, true, cancelToken, m_copybuffer);
+ if (SupportsStreaming)
+ {
+ using (var writestream = systemIO.FileOpenWrite(GetRemoteName(remotename)))
+ await Utility.Utility.CopyStreamAsync(stream, writestream, true, cancelToken, m_copybuffer);
+ }
+ else
+ {
+ var filename = (stream as FauxStream).Filename;
+ var path = GetRemoteName(remotename);
+ if (m_moveFile)
+ {
+ if (systemIO.FileExists(path))
+ systemIO.FileDelete(path);
+
+ systemIO.FileMove(filename, path);
+ }
+ else
+ systemIO.FileCopy(filename, path, true);
+ }
}
#endif
- public void Get(string remotename, System.IO.Stream stream)
- {
- // FileOpenRead has flags System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read
- using (System.IO.FileStream readstream = systemIO.FileOpenRead(GetRemoteName(remotename)))
- Utility.Utility.CopyStream(readstream, stream, true, m_copybuffer);
- }
-
- public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
- {
- string path = GetRemoteName(remotename);
- if (m_moveFile)
+ public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
+ {
+ if (SupportsStreaming)
{
- if (systemIO.FileExists(path))
- systemIO.FileDelete(path);
-
- systemIO.FileMove(filename, path);
+ // FileOpenRead has flags System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read
+ using (var readstream = systemIO.FileOpenRead(GetRemoteName(remotename)))
+ await Utility.Utility.CopyStreamAsync(readstream, stream, true, cancelToken, m_copybuffer);
}
else
- systemIO.FileCopy(filename, path, true);
-
- return Task.FromResult(true);
+ {
+ var filename = (stream as FauxStream).Filename;
+ var path = GetRemoteName(remotename);
+ if (m_moveFile)
+ {
+ if (systemIO.FileExists(filename))
+ systemIO.FileDelete(filename);
+
+ systemIO.FileMove(path, filename);
+ }
+ else
+ systemIO.FileCopy(path, filename, true);
+ }
}
- public void Get(string remotename, string filename)
- {
- systemIO.FileCopy(GetRemoteName(remotename), filename, true);
- }
- public void Delete(string remotename)
+ public Task DeleteAsync(string remotename, CancellationToken cancelToken)
{
systemIO.FileDelete(GetRemoteName(remotename));
+ return Task.FromResult(true);
}
public IList<ICommandLineArgument> SupportedCommands
@@ -259,17 +280,16 @@ namespace Duplicati.Library.Backend
}
}
- public void Test()
- {
- this.TestList();
- }
+ public Task TestAsync(CancellationToken cancelToken)
+ =>this.TestListAsync(cancelToken);
- public void CreateFolder()
+ public Task CreateFolderAsync(CancellationToken cancelToken)
{
if (systemIO.DirectoryExists(m_path))
throw new FolderAreadyExistedException();
systemIO.DirectoryCreate(m_path);
+ return Task.FromResult(true);
}
#endregion
@@ -347,6 +367,8 @@ namespace Duplicati.Library.Backend
get { return null; }
}
+ public bool SupportsStreaming => !(m_no_streaming || m_moveFile);
+
public void Rename(string oldname, string newname)
{
var source = GetRemoteName(oldname);
diff --git a/Duplicati/Library/Backend/File/Strings.cs b/Duplicati/Library/Backend/File/Strings.cs
index 3bb2659b6..82f6b3dbe 100644
--- a/Duplicati/Library/Backend/File/Strings.cs
+++ b/Duplicati/Library/Backend/File/Strings.cs
@@ -1,5 +1,5 @@
using Duplicati.Library.Localization.Short;
-namespace Duplicati.Library.Backend.Strings {
+namespace Duplicati.Library.Backend.File.Strings {
internal static class FileBackend {
public static string AlternateDestinationMarkerLong(string optionname) { return LC.L(@"This option only works when the --{0} option is also specified. If there are alternate paths specified, this option indicates the name of a marker file that must be present in the folder. This can be used to handle situations where an external drive changes drive letter or mount point. By ensuring that a certain file exists, it is possible to prevent writing data to an unwanted external drive. The contents of the file are never examined, only file existence.", optionname); }
public static string AlternateDestinationMarkerShort { get { return LC.L(@"Look for a file in the destination folder"); } }
diff --git a/Duplicati/Library/Backend/File/Win32.cs b/Duplicati/Library/Backend/File/Win32.cs
index a0c69b672..31225e686 100644
--- a/Duplicati/Library/Backend/File/Win32.cs
+++ b/Duplicati/Library/Backend/File/Win32.cs
@@ -22,7 +22,7 @@ using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
-namespace Duplicati.Library.Backend
+namespace Duplicati.Library.Backend.File
{
internal class Win32
{
diff --git a/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs b/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs
index 33accbd2e..c41f888d3 100644
--- a/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs
+++ b/Duplicati/Library/Backend/OAuthHelper/JSONWebHelper.cs
@@ -19,63 +19,66 @@ using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
-using System.Net;
+using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Net.Http.Headers;
namespace Duplicati.Library
{
- public class JSONWebHelper
+ public class JSONWebHelper : IDisposable
{
+ /// <summary>
+ /// The User-Agent default value
+ /// </summary>
public static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
+ /// <summary>
+ /// The currently set User-Agent value
+ /// </summary>
private readonly string m_user_agent;
+ /// <summary>
+ /// The URL to perform the OAuth login
+ /// </summary>
public string OAuthLoginUrl { get; protected set; }
+ /// <summary>
+ /// Gets the current User-Agent value
+ /// </summary>
public string UserAgent { get { return m_user_agent; } }
- public event Action<HttpWebRequest> CreateSetupHelper;
- private static readonly byte[] crlf = Encoding.UTF8.GetBytes("\r\n");
+ /// <summary>
+ /// A callback method to help set up the request
+ /// </summary>
+ public event Action<HttpRequestMessage> CreateSetupHelper;
+ /// <summary>
+ /// The internal client used to perform the requests
+ /// </summary>
+ protected readonly HttpClient m_client = new HttpClient();
+ /// <summary>
+ /// Constructs a new JSONWebHelper
+ /// </summary>
+ /// <param name="useragent">The User-Agent string to use</param>
public JSONWebHelper(string useragent = null)
{
m_user_agent = useragent ?? USER_AGENT;
}
- public virtual HttpWebRequest CreateRequest(string url, string method = null)
+ /// <summary>
+ /// Method used to create the request
+ /// </summary>
+ /// <param name="url">The target url</param>
+ /// <param name="method">The method to use</param>
+ /// <returns>A created request</returns>
+ public virtual Task<HttpRequestMessage> CreateRequestAsync(string url, string method, CancellationToken cancelToken)
{
- var req = (HttpWebRequest)WebRequest.Create(url);
- req.UserAgent = UserAgent;
- if (method != null)
- req.Method = method;
+ var req = new HttpRequestMessage(new HttpMethod(method ?? "GET"), url);
+ req.Headers.UserAgent.Clear();
+ req.Headers.UserAgent.ParseAdd(UserAgent);
if (CreateSetupHelper != null)
CreateSetupHelper(req);
- return req;
- }
-
- /// <summary>
- /// Performs a multipart post and parses the response as JSON
- /// </summary>
- /// <returns>The parsed JSON item.</returns>
- /// <param name="url">The url to post to.</param>
- /// <param name="parts">The multipart items.</param>
- /// <typeparam name="T">The return type parameter.</typeparam>
- public virtual T PostMultipartAndGetJSONData<T>(string url, params MultipartItem[] parts)
- {
- return ReadJSONResponse<T>(PostMultipart(url, null, parts));
- }
-
- /// <summary>
- /// Performs a multipart post and parses the response as JSON
- /// </summary>
- /// <returns>The parsed JSON item.</returns>
- /// <param name="url">The url to post to.</param>
- /// <param name="parts">The multipart items.</param>
- /// <param name="setup">The optional setup callback method.</param>
- /// <typeparam name="T">The return type parameter.</typeparam>
- public virtual T PostMultipartAndGetJSONData<T>(string url, Action<HttpWebRequest> setup = null, params MultipartItem[] parts)
- {
- return ReadJSONResponse<T>(PostMultipart(url, setup, parts));
+ return Task.FromResult(req);
}
/// <summary>
@@ -87,40 +90,20 @@ namespace Duplicati.Library
/// <param name="cancelToken">Token to cancel the operation.</param>
/// <param name="parts">The multipart items.</param>
/// <typeparam name="T">The return type parameter.</typeparam>
- public virtual async Task<T> PostMultipartAndGetJSONDataAsync<T>(string url, Action<HttpWebRequest> setup, CancellationToken cancelToken, params MultipartItem[] parts)
+ public virtual async Task<T> PostMultipartAndGetJSONDataAsync<T>(string url, Action<HttpRequestMessage> setup, CancellationToken cancelToken, params MultipartItem[] parts)
{
var response = await PostMultipartAsync(url, setup, cancelToken, parts).ConfigureAwait(false);
- return ReadJSONResponse<T>(response);
+ return await ReadJSONResponseAsync<T>(response, cancelToken);
}
/// <summary>
- /// Performs a multipart post
+ /// List of protected headers that need special handling to se
/// </summary>
- /// <returns>The response.</returns>
- /// <param name="url">The url to post to.</param>
- /// <param name="parts">The multipart items.</param>
- /// <param name="setup">The optional setup callback method.</param>
- public virtual HttpWebResponse PostMultipart(string url, Action<HttpWebRequest> setup = null, params MultipartItem[] parts)
- {
- CreateBoundary(out var boundary, out var bodyTerminator);
-
- var req = PreparePostMultipart(url, setup, boundary, bodyTerminator, out var headers, parts);
- var areq = new AsyncHttpRequest(req);
-
- using (var rs = areq.GetRequestStream())
- {
- foreach(var p in headers)
- {
- rs.Write(p.Header, 0, p.Header.Length);
- Utility.Utility.CopyStream(p.Part.ContentData, rs);
- rs.Write(crlf, 0, crlf.Length);
- }
-
- rs.Write(bodyTerminator, 0, bodyTerminator.Length);
- }
-
- return GetResponse(areq);
- }
+ /// <param name="k">The header name</param>
+ /// <returns><c>true</c> if the header is protected; <c>false</c> otherwise</returns>
+ private static bool IsProtectedHeader(string k)
+ => new [] { "Content-Type", "Content-Disposition", "Content-Length" }
+ .Any(x => string.Equals(x, k, StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Performs a multipart post
@@ -130,84 +113,55 @@ namespace Duplicati.Library
/// <param name="parts">The multipart items.</param>
/// <param name="setup">The optional setup callback method.</param>
/// <param name="cancelToken">Token to cancel the operation.</param>
- public virtual async Task<HttpWebResponse> PostMultipartAsync(string url, Action<HttpWebRequest> setup, CancellationToken cancelToken, params MultipartItem[] parts)
+ public virtual async Task<HttpResponseMessage> PostMultipartAsync(string url, Action<HttpRequestMessage> setup, CancellationToken cancelToken, params MultipartItem[] parts)
{
- CreateBoundary(out var boundary, out var bodyTerminator);
-
- var req = PreparePostMultipart(url, setup, boundary, bodyTerminator, out var headers, parts);
- var areq = new AsyncHttpRequest(req);
- var buffer = new byte[Utility.Utility.DEFAULT_BUFFER_SIZE];
-
- using (var rs = areq.GetRequestStream())
+ using(var mpdata = new MultipartFormDataContent(CreateBoundary()))
{
- foreach (var p in headers)
+ foreach(var p in parts)
{
- await rs.WriteAsync(p.Header, 0, p.Header.Length, cancelToken).ConfigureAwait(false);
- await Utility.Utility.CopyStreamAsync(p.Part.ContentData, rs, tryRewindSource: true, cancelToken:cancelToken, buf: buffer).ConfigureAwait(false);
- await rs.WriteAsync(crlf, 0, crlf.Length, cancelToken).ConfigureAwait(false);
- }
+ var sc = new StreamContent(p.ContentData);
+ foreach(var h in p.Headers)
+ if (!IsProtectedHeader(h.Key))
+ sc.Headers.Add(h.Key, h.Value);
- await rs.WriteAsync(bodyTerminator, 0, bodyTerminator.Length, cancelToken).ConfigureAwait(false);
- }
+ if (p.ContentLength >= 0)
+ sc.Headers.ContentLength = p.ContentLength;
- return (HttpWebResponse)(await req.GetResponseAsync().ConfigureAwait(false));
- }
+ if (!string.IsNullOrWhiteSpace(p.ContentTypeName))
+ sc.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(p.Headers["Content-Disposition"]);
- protected virtual HttpWebRequest PreparePostMultipart(string url, Action<HttpWebRequest> setup, string boundary, byte[] bodyTerminator, out HeaderPart[] headers, params MultipartItem[] parts)
- {
- headers =
- (from p in parts
- select new HeaderPart(
- Encoding.UTF8.GetBytes(
- "--" + boundary + "\r\n"
- + string.Join("",
- from n in p.Headers
- select string.Format("{0}: {1}\r\n", n.Key, n.Value)
- ) + "\r\n"),
- p)).ToArray();
-
- var envelopesize = headers.Sum(x => x.Header.Length + crlf.Length) + bodyTerminator.Length;
- var datasize = parts.Sum(x => x.ContentData.Length);
-
- var req = CreateRequest(url);
-
- req.Method = "POST";
- req.ContentType = "multipart/form-data; boundary=" + boundary;
- req.ContentLength = envelopesize + datasize;
+ if (!string.IsNullOrWhiteSpace(p.ContentType))
+ sc.Headers.ContentType = MediaTypeHeaderValue.Parse(p.ContentType);
- setup?.Invoke(req);
- return req;
- }
+ mpdata.Add(sc);
+ }
- private static void CreateBoundary(out string boundary, out byte[] bodyterminator)
- {
- boundary = "----DuplicatiFormBoundary" + Guid.NewGuid().ToString("N");
- bodyterminator = Encoding.UTF8.GetBytes("--" + boundary + "--");
+ var req = await CreateRequestAsync(url, "POST", cancelToken);
+ setup?.Invoke(req);
+
+ req.Content = mpdata;
+
+ return await m_client.SendAsync(req, cancelToken);
+ }
}
+ /// <summary>
+ /// Creates a random form bondary string
+ /// </summary>
+ private static string CreateBoundary()
+ => "----DuplicatiFormBoundary" + Guid.NewGuid().ToString("N");
/// <summary>
/// Executes a web request and json-deserializes the results as the specified type
/// </summary>
/// <returns>The deserialized JSON data.</returns>
/// <param name="url">The remote URL</param>
+ /// <param name="cancelToken">Token to cancel the operation.</param>
/// <param name="setup">A callback method that can be used to customize the request, e.g. by setting the method, content-type and headers.</param>
/// <param name="setupbodyreq">A callback method that can be used to submit data into the body of the request.</param>
/// <typeparam name="T">The type of data to return.</typeparam>
- public virtual T GetJSONData<T>(string url, Action<HttpWebRequest> setup = null, Action<AsyncHttpRequest> setupbodyreq = null)
- {
- var req = CreateRequest(url);
-
- if (setup != null)
- setup(req);
-
- var areq = new AsyncHttpRequest(req);
-
- if (setupbodyreq != null)
- setupbodyreq(areq);
-
- return ReadJSONResponse<T>(areq);
- }
+ public virtual Task<T> GetJSONDataAsync<T>(string url, CancellationToken cancelToken)
+ => GetJSONDataAsync<T>(url, null, cancelToken);
/// <summary>
/// Executes a web request and json-deserializes the results as the specified type
@@ -218,16 +172,12 @@ namespace Duplicati.Library
/// <param name="setup">A callback method that can be used to customize the request, e.g. by setting the method, content-type and headers.</param>
/// <param name="setupbodyreq">A callback method that can be used to submit data into the body of the request.</param>
/// <typeparam name="T">The type of data to return.</typeparam>
- public virtual async Task<T> GetJSONDataAsync<T>(string url, CancellationToken cancelToken, Action<HttpWebRequest> setup = null, Func<AsyncHttpRequest, CancellationToken, Task> setupbodyreq = null)
+ public virtual async Task<T> GetJSONDataAsync<T>(string url, Action<HttpRequestMessage> setup, CancellationToken cancelToken)
{
- var req = CreateRequest(url);
+ var req = await CreateRequestAsync(url, null, cancelToken);
setup?.Invoke(req);
- var areq = new AsyncHttpRequest(req);
- if (setupbodyreq != null)
- await setupbodyreq(areq, cancelToken).ConfigureAwait(false);
-
- return await ReadJSONResponseAsync<T>(areq, cancelToken).ConfigureAwait(false);
+ return await ReadJSONResponseAsync<T>(req, cancelToken).ConfigureAwait(false);
}
/// <summary>
@@ -237,29 +187,36 @@ namespace Duplicati.Library
/// <param name="url">The remote URL</param>
/// <param name="item">The data to json-serialize and POST in the request</param>
/// <param name="method">Alternate HTTP method to use</param>
+ /// <param name="cancelToken">Token to cancel the operation.</param>
/// <typeparam name="T">The type of data to return.</typeparam>
- public virtual T PostAndGetJSONData<T>(string url, object item, string method = null)
+ public virtual Task<T> PostAndGetJSONDataAsync<T>(string url, object item, string method, CancellationToken cancelToken)
{
var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item));
- return GetJSONData<T>(
+ return GetJSONDataAsync<T>(
url,
req =>
- {
- req.Method = method ?? "POST";
- req.ContentType = "application/json; charset=utf-8";
- req.ContentLength = data.Length;
+ {
+ req.Method = new HttpMethod(method ?? "POST");
+ var sc = new StreamContent(new MemoryStream(data));
+ sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
+ sc.Headers.ContentLength = data.Length;
+ req.Content = sc;
},
-
- req =>
- {
- using(var rs = req.GetRequestStream())
- rs.Write(data, 0, data.Length);
- }
+ cancelToken
);
}
- public virtual T ReadJSONResponse<T>(string url, object requestdata = null, string method = null)
+ /// <summary>
+ /// Performs a POST request with the given data, serialized as JSON, and returns the deserialized JSON result
+ /// </summary>
+ /// <param name="url">The remote URL</param>
+ /// <param name="requestdata">The JSON object</param>
+ /// <param name="method">The alternate method</param>
+ /// <param name="cancelToken">The cancellation token</param>
+ /// <typeparam name="T">The type of data to return.</typeparam>
+ /// <returns>The deserialized JSON data.</returns>
+ public virtual Task<T> ReadJSONResponseAsync<T>(string url, object requestdata, string method, CancellationToken cancelToken)
{
if (requestdata is string)
throw new ArgumentException("Cannot send string object as data");
@@ -267,30 +224,29 @@ namespace Duplicati.Library
if (method == null && requestdata != null)
method = "POST";
- return ReadJSONResponse<T>(CreateRequest(url, method), requestdata);
- }
-
- public virtual T ReadJSONResponse<T>(HttpWebRequest req, object requestdata = null)
- {
- return ReadJSONResponse<T>(new AsyncHttpRequest(req), requestdata);
+ return PostAndGetJSONDataAsync<T>(url, requestdata, method, cancelToken);
}
- public virtual T ReadJSONResponse<T>(AsyncHttpRequest req, object requestdata = null)
- {
- using(var resp = GetResponse(req, requestdata))
- return ReadJSONResponse<T>(resp);
- }
-
- public virtual async Task<T> ReadJSONResponseAsync<T>(AsyncHttpRequest req, CancellationToken cancelToken, object requestdata = null)
- {
- using (var resp = await GetResponseAsync(req, cancelToken, requestdata).ConfigureAwait(false))
- return ReadJSONResponse<T>(resp);
- }
+ /// <summary>
+ /// Performs the request and returns the deserialized JSON result
+ /// </summary>
+ /// <param name="req"></param>
+ /// <param name="cancelToken">The cancellation token</param>
+ /// <typeparam name="T">The type of data to return.</typeparam>
+ /// <returns>The deserialized JSON data.</returns>
+ public virtual async Task<T> ReadJSONResponseAsync<T>(HttpRequestMessage req, CancellationToken cancelToken)
+ => await ReadJSONResponseAsync<T>(await m_client.SendAsync(req, cancelToken), cancelToken);
- public virtual T ReadJSONResponse<T>(HttpWebResponse resp)
+ /// <summary>
+ /// Extracts the JSON data an deserializes it, handling invalid JSON data with an improved exception message
+ /// </summary>
+ /// <param name="resp">The response to read from</param>
+ /// <param name="cancelToken">The cancellation token</param>
+ /// <typeparam name="T">The type of data to return.</typeparam>
+ /// <returns>The deserialized JSON data.</returns>
+ public virtual async Task<T> ReadJSONResponseAsync<T>(HttpResponseMessage resp, CancellationToken cancelToken)
{
- using (var rs = Duplicati.Library.Utility.AsyncHttpRequest.TrySetTimeout(resp.GetResponseStream()))
- using(var ps = new StreamPeekReader(rs))
+ using(var ps = new StreamPeekReader(await resp.Content.ReadAsStreamAsync()))
{
try
{
@@ -314,11 +270,20 @@ namespace Duplicati.Library
/// which can throw another, more meaningful exception
/// </summary>
/// <param name="ex">The exception being processed.</param>
- protected virtual void ParseException(Exception ex)
+ protected virtual Task ParseExceptionAsync(Exception ex)
{
+ return Task.FromResult(true);
}
- public HttpWebResponse GetResponseWithoutException(string url, object requestdata = null, string method = null)
+ /// <summary>
+ /// Performs a POST request with the given data, response even if the error code is an error
+ /// </summary>
+ /// <param name="url">The remote URL</param>
+ /// <param name="requestdata">The JSON object</param>
+ /// <param name="method">The alternate method</param>
+ /// <param name="cancelToken">The cancellation token</param>
+ /// <returns>The response.</returns>
+ public async Task<HttpResponseMessage> GetResponseWithoutExceptionAsync(string url, object requestdata, string method, CancellationToken cancelToken)
{
if (requestdata is string)
throw new ArgumentException("Cannot send string object as data");
@@ -326,105 +291,44 @@ namespace Duplicati.Library
if (method == null && requestdata != null)
method = "POST";
- return GetResponseWithoutException(CreateRequest(url, method), requestdata);
- }
-
- public HttpWebResponse GetResponseWithoutException(HttpWebRequest req, object requestdata = null)
- {
- return GetResponseWithoutException(new AsyncHttpRequest(req), requestdata);
+ return await GetResponseWithoutExceptionAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken);
}
- public HttpWebResponse GetResponseWithoutException(AsyncHttpRequest req, object requestdata = null)
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="req">The request to execute</param>
+ /// <param name="requestdata">The content, either a stream or an object</param>
+ /// <param name="cancelToken">The cancellation token</param>
+ /// <returns>The response.</returns>
+ public async Task<HttpResponseMessage> GetResponseWithoutExceptionAsync(HttpRequestMessage req, object requestdata, CancellationToken cancelToken)
{
- try
+ if (requestdata != null)
{
- if (requestdata != null)
+ if (requestdata is Stream stream)
{
- if (requestdata is Stream stream)
- {
- req.Request.ContentLength = stream.Length;
- if (string.IsNullOrEmpty(req.Request.ContentType))
- req.Request.ContentType = "application/octet-stream";
-
- using (var rs = req.GetRequestStream())
- Library.Utility.Utility.CopyStream(stream, rs);
- }
- else
- {
- var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata));
- req.Request.ContentLength = data.Length;
- req.Request.ContentType = "application/json; charset=UTF-8";
-
- using (var rs = req.GetRequestStream())
- rs.Write(data, 0, data.Length);
- }
+ var sc = new StreamContent(stream);
+ sc.Headers.ContentLength = stream.Length;
+ sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
+ req.Content = sc;
}
+ else
+ {
+ var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata));
+ var sc = new StreamContent(new MemoryStream(data));
+ sc.Headers.ContentLength = data.Length;
- return (HttpWebResponse)req.GetResponse();
- }
- catch(WebException wex)
- {
- if (wex.Response is HttpWebResponse response)
- return response;
-
- throw;
- }
- }
-
- public async Task<HttpWebResponse> GetResponseWithoutExceptionAsync(string url, CancellationToken cancelToken, object requestdata = null, string method = null)
- {
- if (requestdata is string)
- throw new ArgumentException("Cannot send string object as data");
-
- if (method == null && requestdata != null)
- method = "POST";
-
- return await GetResponseWithoutExceptionAsync(CreateRequest(url, method), cancelToken, requestdata).ConfigureAwait(false);
- }
+ sc.Headers.ContentLength = data.Length;
+ sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=UTF-8");
- public async Task<HttpWebResponse> GetResponseWithoutExceptionAsync(HttpWebRequest req, CancellationToken cancelToken, object requestdata = null)
- {
- return await GetResponseWithoutExceptionAsync(new AsyncHttpRequest(req), cancelToken, requestdata).ConfigureAwait(false);
- }
-
- public async Task<HttpWebResponse> GetResponseWithoutExceptionAsync(AsyncHttpRequest req, CancellationToken cancelToken, object requestdata = null)
- {
- try
- {
- if (requestdata != null)
- {
- if (requestdata is System.IO.Stream stream)
- {
- req.Request.ContentLength = stream.Length;
- if (string.IsNullOrEmpty(req.Request.ContentType))
- req.Request.ContentType = "application/octet-stream";
-
- using (var rs = req.GetRequestStream())
- await Utility.Utility.CopyStreamAsync(stream, rs, cancelToken).ConfigureAwait(false);
- }
- else
- {
- var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata));
- req.Request.ContentLength = data.Length;
- req.Request.ContentType = "application/json; charset=UTF-8";
-
- using (var rs = req.GetRequestStream())
- await rs.WriteAsync(data, 0, data.Length, cancelToken).ConfigureAwait(false);
- }
+ req.Content = sc;
}
-
- return (HttpWebResponse)req.GetResponse();
}
- catch (WebException wex)
- {
- if (wex.Response is HttpWebResponse response)
- return response;
- throw;
- }
+ return await m_client.SendAsync(req, cancelToken);
}
- public HttpWebResponse GetResponse(string url, object requestdata = null, string method = null)
+ public async Task<HttpResponseMessage> GetResponseAsync(string url, object requestdata, string method, CancellationToken cancelToken)
{
if (requestdata is string)
throw new ArgumentException("Cannot send string object as data");
@@ -432,105 +336,39 @@ namespace Duplicati.Library
if (method == null && requestdata != null)
method = "POST";
- return GetResponse(CreateRequest(url, method), requestdata);
+ return await GetResponseAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken);
}
- public HttpWebResponse GetResponse(HttpWebRequest req, object requestdata = null)
- {
- return GetResponse(new AsyncHttpRequest(req), requestdata);
- }
-
- public HttpWebResponse GetResponse(AsyncHttpRequest req, object requestdata = null)
+ public async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage req, object requestdata, CancellationToken cancelToken)
{
try
{
- if (requestdata != null)
- {
- if (requestdata is Stream stream)
- {
- req.Request.ContentLength = stream.Length;
- if (string.IsNullOrEmpty(req.Request.ContentType))
- req.Request.ContentType = "application/octet-stream";
-
- using (var rs = req.GetRequestStream())
- Library.Utility.Utility.CopyStream(stream, rs);
- }
- else
- {
- var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata));
- req.Request.ContentLength = data.Length;
- req.Request.ContentType = "application/json; charset=UTF-8";
-
- using (var rs = req.GetRequestStream())
- rs.Write(data, 0, data.Length);
- }
- }
-
- return (HttpWebResponse)req.GetResponse();
+ var resp = await GetResponseWithoutExceptionAsync(req, requestdata, cancelToken);
+ if (!resp.IsSuccessStatusCode)
+ throw new HttpRequestStatusException(resp);
+ resp.EnsureSuccessStatusCode();
+ return resp;
}
catch (Exception ex)
{
- ParseException(ex);
+ await ParseExceptionAsync(ex);
throw;
}
}
- public async Task<HttpWebResponse> GetResponseAsync(string url, CancellationToken cancelToken, object requestdata = null, string method = null)
- {
- if (requestdata is string)
- throw new ArgumentException("Cannot send string object as data");
-
- if (method == null && requestdata != null)
- method = "POST";
-
- var areq = new AsyncHttpRequest(CreateRequest(url, method));
- return await GetResponseAsync(areq, cancelToken, requestdata).ConfigureAwait(false);
- }
-
- public async Task<HttpWebResponse> GetResponseAsync(AsyncHttpRequest req, CancellationToken cancelToken, object requestdata = null)
+ public void Dispose()
{
- try
- {
- if (requestdata != null)
- {
- if (requestdata is System.IO.Stream stream)
- {
- req.Request.ContentLength = stream.Length;
- if (string.IsNullOrEmpty(req.Request.ContentType))
- req.Request.ContentType = "application/octet-stream";
-
- using (var rs = req.GetRequestStream())
- await Utility.Utility.CopyStreamAsync(stream, rs, cancelToken).ConfigureAwait(false);
- }
- else
- {
- var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(requestdata));
- req.Request.ContentLength = data.Length;
- req.Request.ContentType = "application/json; charset=UTF-8";
-
- using (var rs = req.GetRequestStream())
- await rs.WriteAsync(data, 0, data.Length, cancelToken).ConfigureAwait(false);
- }
- }
-
- return (HttpWebResponse)req.GetResponse();
- }
- catch (Exception ex)
- {
- ParseException(ex);
- throw;
- }
+ m_client.Dispose();
}
- protected class HeaderPart
+ public class HttpRequestStatusException : HttpRequestException
{
- public readonly byte[] Header;
- public readonly MultipartItem Part;
+ public readonly HttpResponseMessage Response;
- public HeaderPart(byte[] header, MultipartItem part)
+ public HttpRequestStatusException(HttpResponseMessage resp)
+ : base(resp.ReasonPhrase)
{
- Header = header;
- Part = part;
+ Response = resp;
}
}
diff --git a/Duplicati/Library/Backend/OAuthHelper/MultipartItem.cs b/Duplicati/Library/Backend/OAuthHelper/MultipartItem.cs
index 67176ec11..b5f820928 100644
--- a/Duplicati/Library/Backend/OAuthHelper/MultipartItem.cs
+++ b/Duplicati/Library/Backend/OAuthHelper/MultipartItem.cs
@@ -100,6 +100,20 @@ namespace Duplicati.Library
}
}
+ public string ContentTypeFileName
+ {
+ get
+ {
+ string v;
+ Headers.TryGetValue("Content-Disposition", out v);
+ if (string.IsNullOrWhiteSpace(v))
+ return null;
+
+ var m = new System.Text.RegularExpressions.Regex("filename=\"(?<name>[^\"]+)\"").Match(v);
+ return m.Success ? m.Groups["name"].Value : null;
+ }
+ }
+
public long ContentLength
{
get
diff --git a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs
index 007376cc0..9c49f0504 100644
--- a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs
+++ b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs
@@ -1,4 +1,6 @@
-// Copyright (C) 2015, The Duplicati Team
+using System.Linq;
+using System.Threading;
+// 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
@@ -18,7 +20,9 @@ using System;
using System.Net;
using Duplicati.Library.Utility;
using System.Collections.Generic;
-using System.Web;
+using System.Net.Http;
+using System.Threading.Tasks;
+
namespace Duplicati.Library
{
/// <summary>
@@ -94,87 +98,86 @@ namespace Duplicati.Library
throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.MissingAuthID(OAuthLoginUrl), "MissingAuthID");
}
- public T GetTokenResponse<T>()
+ public async Task<T> GetTokenResponseAsync<T>(CancellationToken cancelToken)
{
- var req = CreateRequest(OAuthContextSettings.ServerURL);
- req.Headers["X-AuthID"] = m_authid;
- req.Timeout = (int)TimeSpan.FromSeconds(25).TotalMilliseconds;
+ var req = await CreateRequestAsync(OAuthContextSettings.ServerURL, null, cancelToken);
+ req.Headers.Add("X-AuthID", m_authid);
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(25));
+ if (cancelToken.CanBeCanceled)
+ cancelToken.Register(() => cts.Cancel());
- return ReadJSONResponse<T>(req);
+ return await ReadJSONResponseAsync<T>(req, cts.Token);
}
- public override HttpWebRequest CreateRequest(string url, string method = null)
+ public override Task<HttpRequestMessage> CreateRequestAsync(string url, string method, CancellationToken cancelToken)
{
- return this.CreateRequest(url, method, false);
+ return this.CreateRequestAsync(url, method, false, cancelToken);
}
- public HttpWebRequest CreateRequest(string url, string method, bool noAuthorization)
+ public async Task<HttpRequestMessage> CreateRequestAsync(string url, string method, bool noAuthorization, CancellationToken cancelToken)
{
- var r = base.CreateRequest(url, method);
+ var r = await base.CreateRequestAsync(url, method, cancelToken);
if (!noAuthorization && AutoAuthHeader && !string.Equals(OAuthContextSettings.ServerURL, url))
- r.Headers["Authorization"] = string.Format("Bearer {0}", AccessToken);
+ r.Headers.Add("Authorization", string.Format("Bearer {0}", GetAccessTokenAsync(new CancellationTokenSource(TimeSpan.FromSeconds(25)).Token).Result));
return r;
}
- public string AccessToken
+ public async Task<string> GetAccessTokenAsync(CancellationToken cancelToken)
{
- get
+ if (AccessTokenOnly)
+ return m_authid;
+
+ if (m_token == null || m_tokenExpires < DateTime.UtcNow)
{
- if (AccessTokenOnly)
- return m_authid;
+ var retries = 0;
- if (m_token == null || m_tokenExpires < DateTime.UtcNow)
+ while(true)
{
- var retries = 0;
-
- while(true)
+ try
{
- try
- {
- var res = GetTokenResponse<OAuth_Service_Response>();
+ var res = await GetTokenResponseAsync<OAuth_Service_Response>(cancelToken);
- m_tokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30);
- if (!string.IsNullOrWhiteSpace(res.v2_authid))
- m_authid = res.v2_authid;
- return m_token = res.access_token;
- }
- catch (Exception ex)
+ m_tokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30);
+ if (!string.IsNullOrWhiteSpace(res.v2_authid))
+ m_authid = res.v2_authid;
+ return m_token = res.access_token;
+ }
+ catch (Exception ex)
+ {
+ var msg = ex.Message;
+ var clienterror = false;
+ if (ex is HttpRequestStatusException exception)
{
- var msg = ex.Message;
- var clienterror = false;
- if (ex is WebException exception)
+ var resp = exception.Response;
+ if (resp != null)
{
- var resp = exception.Response as HttpWebResponse;
- if (resp != null)
+ msg = resp.Headers.GetValues("X-Reason").FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(msg))
+ msg = resp.ReasonPhrase;
+
+ if (resp.StatusCode == HttpStatusCode.ServiceUnavailable)
{
- msg = resp.Headers["X-Reason"];
- if (string.IsNullOrWhiteSpace(msg))
- msg = resp.StatusDescription;
-
- if (resp.StatusCode == HttpStatusCode.ServiceUnavailable)
- {
- if (msg == resp.StatusDescription)
- throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.OverQuotaError, "OAuthOverQuotaError");
- else
- throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), "OAuthLoginError", exception);
- }
-
- //Fail faster on client errors
- clienterror = (int)resp.StatusCode >= 400 && (int)resp.StatusCode <= 499;
+ if (msg == resp.ReasonPhrase)
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.OverQuotaError, "OAuthOverQuotaError");
+ else
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), "OAuthLoginError", exception);
}
+
+ //Fail faster on client errors
+ clienterror = (int)resp.StatusCode >= 400 && (int)resp.StatusCode <= 499;
}
+ }
- if (retries >= (clienterror ? 1 : 5))
- throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), "OAuthLoginError", ex);
+ if (retries >= (clienterror ? 1 : 5))
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl), "OAuthLoginError", ex);
- System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
- retries++;
- }
+ System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
+ retries++;
}
}
-
- return m_token;
}
+
+ return m_token;
}
public void ThrowOverQuotaError()
diff --git a/Duplicati/Library/Backend/OAuthHelper/OAuthHttpMessageHandler.cs b/Duplicati/Library/Backend/OAuthHelper/OAuthHttpMessageHandler.cs
index c77dcad3a..9980e3747 100644
--- a/Duplicati/Library/Backend/OAuthHelper/OAuthHttpMessageHandler.cs
+++ b/Duplicati/Library/Backend/OAuthHelper/OAuthHttpMessageHandler.cs
@@ -1,4 +1,5 @@
-// Copyright (C) 2018, The Duplicati Team
+using System;
+// Copyright (C) 2018, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
@@ -46,14 +47,14 @@ namespace Duplicati.Library
return request;
}
- protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!request.Properties.ContainsKey(DISABLE_AUTHENTICATION_PROPERTY))
{
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.m_oauth.AccessToken);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await this.m_oauth.GetAccessTokenAsync(cancellationToken));
}
- return base.SendAsync(request, cancellationToken);
+ return await base.SendAsync(request, cancellationToken);
}
}
}
diff --git a/Duplicati/Library/Common/Platform/Platform.cs b/Duplicati/Library/Common/Platform/Platform.cs
index 69d3a04ad..c879c5850 100644
--- a/Duplicati/Library/Common/Platform/Platform.cs
+++ b/Duplicati/Library/Common/Platform/Platform.cs
@@ -1,4 +1,5 @@
-// Copyright (C) 2018, The Duplicati Team
+using System.Runtime.InteropServices;
+// Copyright (C) 2018, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
@@ -14,11 +15,13 @@
// 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;
-namespace Duplicati.Library.Common
-{
- public static class Platform
- {
+using System;
+using System.Runtime.InteropServices;
+
+namespace Duplicati.Library.Common
+{
+ public static class Platform
+ {
/// <value>
/// Gets or sets a value indicating if the client is Linux/Unix based
/// </value>
@@ -38,9 +41,9 @@ namespace Duplicati.Library.Common
static Platform()
{
- IsClientPosix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX;
+ IsClientOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ IsClientPosix = IsClientOSX || RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
IsClientWindows = !IsClientPosix;
- IsClientOSX = IsClientPosix && "Darwin".Equals(_RetrieveUname(false));
}
/// <value>
@@ -83,5 +86,5 @@ namespace Duplicati.Library.Common
return null;
}
- }
-}
+ }
+}
diff --git a/Duplicati/Library/Interface/BackendExtensions.cs b/Duplicati/Library/Interface/BackendExtensions.cs
index 486632d76..3487d14ef 100644
--- a/Duplicati/Library/Interface/BackendExtensions.cs
+++ b/Duplicati/Library/Interface/BackendExtensions.cs
@@ -1,4 +1,5 @@
-using System;
+using System.Threading;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -12,17 +13,40 @@ namespace Duplicati.Library.Interface
public static class BackendExtensions
{
/// <summary>
- /// Tests a backend by invoking the List() method.
+ /// Tests a backend by invoking the ListAsync() method.
/// As long as the iteration can either complete or find at least one file without throwing, the test is successful
/// </summary>
/// <param name="backend">Backend to test</param>
- public static void TestList(this IBackend backend)
+ /// <param name="token">The cancellation token to use</param>
+ /// <returns>An awaitable task</returns>
+ public static async Task TestListAsync(this IBackend backend, CancellationToken token)
{
- // If we can iterate successfully, even if it's empty, then the backend test is successful
- foreach (IFileEntry file in backend.List())
+ if (backend is IBackendPagination backendPagination)
{
- break;
+ await foreach(var res in backendPagination.ListEnumerableAsync(token))
+ break;
}
+ else
+ {
+ // If we can iterate successfully, even if it's empty, then the backend test is successful
+ foreach(var res in await backend.ListAsync(token))
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Converts a paginated list into a condensed simple list
+ /// </summary>
+ /// <param name="backend">The pagination enabled backend</param>
+ /// <param name="token">The cancellation token to use</param>
+ /// <returns>The complete list</returns>
+ public static async Task<IList<IFileEntry>> CondensePaginatedListAsync(this IBackendPagination backend, CancellationToken token)
+ {
+ var lst = new List<IFileEntry>();
+ await foreach(var n in backend.ListEnumerableAsync(token))
+ lst.Add(n);
+
+ return lst;
}
}
}
diff --git a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
index 1c11a459d..fdd222070 100644
--- a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
+++ b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
@@ -1,15 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
+ <LangVersion>8.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<Copyright>LGPL, Copyright © Duplicati Team 2021</Copyright>
<Description>The Duplicati library with all publicly exposed interfaces</Description>
+ <RootNamespace>Duplicati.Library.Interface</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Localization\Duplicati.Library.Localization.csproj" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
+ <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" />
+ </ItemGroup>
+
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
diff --git a/Duplicati/Library/Interface/IBackend.cs b/Duplicati/Library/Interface/IBackend.cs
index 6afba958a..e20babbf7 100644
--- a/Duplicati/Library/Interface/IBackend.cs
+++ b/Duplicati/Library/Interface/IBackend.cs
@@ -19,6 +19,7 @@
#endregion
using System;
using System.Collections.Generic;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -47,31 +48,42 @@ namespace Duplicati.Library.Interface
string ProtocolKey { get; }
/// <summary>
+ /// A flag indicating if the backend supports streaming data, or requires a local file target/source
+ /// </summary>
+ bool SupportsStreaming { get; }
+
+ /// <summary>
/// Enumerates a list of files found on the remote location
/// </summary>
+ /// <param name="cancelToken">Token to cancel the operation.</param>
/// <returns>The list of files</returns>
- IEnumerable<IFileEntry> List();
+ Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken);
/// <summary>
/// Puts the content of the file to the url passed
/// </summary>
/// <param name="remotename">The remote filename, relative to the URL</param>
- /// <param name="filename">The local filename</param>
+ /// <param name="source">The stream to read data from</param>
/// <param name="cancelToken">Token to cancel the operation.</param>
- Task PutAsync(string remotename, string filename, CancellationToken cancelToken);
+ /// <returns>An awaitable task</returns>
+ Task PutAsync(string remotename, Stream source, CancellationToken cancelToken);
/// <summary>
/// Downloads a file with the remote data
/// </summary>
/// <param name="remotename">The remote filename, relative to the URL</param>
- /// <param name="filename">The local filename</param>
- void Get(string remotename, string filename);
+ /// <param name="destination">The stream to write data into</param>
+ /// <param name="cancelToken">Token to cancel the operation.</param>
+ /// <returns>An awaitable task</returns>
+ Task GetAsync(string remotename, Stream destination, CancellationToken cancelToken);
/// <summary>
/// Deletes the specified file
/// </summary>
/// <param name="remotename">The remote filename, relative to the URL</param>
- void Delete(string remotename);
+ /// <param name="cancelToken">Token to cancel the operation.</param>
+ /// <returns>An awaitable task</returns>
+ Task DeleteAsync(string remotename, CancellationToken cancelToken);
/// <summary>
/// Gets a list of supported commandline arguments
@@ -94,7 +106,9 @@ namespace Duplicati.Library.Interface
/// If the encountered problem is a missing target &quot;folder&quot;,
/// this method should throw a <see cref="FolderMissingException"/>.
/// </summary>
- void Test();
+ /// <param name="cancelToken">Token to cancel the operation.</param>
+ /// <returns>An awaitable task</returns>
+ Task TestAsync(CancellationToken cancelToken);
/// <summary>
/// The purpose of this method is to create the underlying &quot;folder&quot;.
@@ -104,6 +118,8 @@ namespace Duplicati.Library.Interface
/// a <see cref="FolderMissingException"/> during <see cref="Test"/>,
/// and this method should throw a <see cref="MissingMethodException"/>.
/// </summary>
- void CreateFolder();
+ /// <param name="cancelToken">Token to cancel the operation.</param>
+ /// <returns>An awaitable task</returns>
+ Task CreateFolderAsync(CancellationToken cancelToken);
}
}
diff --git a/Duplicati/Library/Interface/IBackendPagination.cs b/Duplicati/Library/Interface/IBackendPagination.cs
new file mode 100644
index 000000000..f04adb5fb
--- /dev/null
+++ b/Duplicati/Library/Interface/IBackendPagination.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Threading;
+namespace Duplicati.Library.Interface
+{
+ /// <summary>
+ /// A backend interface that adds support for long file listings by allowing each item to be fetched in turn
+ /// </summary>
+ public interface IBackendPagination : IBackend
+ {
+ /// <summary>
+ /// Enumerates a list of files found on the remote location
+ /// </summary>
+ /// <param name="cancelToken">Token to cancel the operation.</param>
+ /// <returns>The list of files</returns>
+ IAsyncEnumerable<IFileEntry> ListEnumerableAsync(CancellationToken cancelToken);
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Interface/IStreamingBackend.cs b/Duplicati/Library/Interface/IStreamingBackend.cs
deleted file mode 100644
index e9779a009..000000000
--- a/Duplicati/Library/Interface/IStreamingBackend.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-#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.Threading;
-using System.Threading.Tasks;
-
-namespace Duplicati.Library.Interface
-{
- /// <summary>
- /// An interface a backend may implement if it supports streaming operations.
- /// Backends that implement this interface can be throttled and correctly shows
- /// the progressbar when transferring data.
- /// </summary>
- public interface IStreamingBackend : IBackend
- {
- /// <summary>
- /// Puts the content of the file to the url passed
- /// </summary>
- /// <param name="remotename">The remote filename, relative to the URL</param>
- /// <param name="stream">The stream to read from</param>
- /// <param name="cancelToken">Token to cancel the operation.</param>
- Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken);
-
- /// <summary>
- /// Downloads a file with the remote data
- /// </summary>
- /// <param name="remotename">The remote filename, relative to the URL</param>
- /// <param name="stream">The stream to write data to</param>
- void Get(string remotename, System.IO.Stream stream);
-
- }
-}
diff --git a/Duplicati/Library/Utility/AsyncHttpRequest.cs b/Duplicati/Library/Utility/AsyncHttpRequest.cs
deleted file mode 100644
index 0ba349d71..000000000
--- a/Duplicati/Library/Utility/AsyncHttpRequest.cs
+++ /dev/null
@@ -1,293 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Net;
-using System.IO;
-using System.Threading;
-
-namespace Duplicati.Library.Utility
-{
- /// <summary>
- /// This class wraps a HttpWebRequest and performs GetRequestStream and GetResponseStream
- /// with async methods while maintaining a synchronous interface
- /// </summary>
- public class AsyncHttpRequest
- {
- /// <summary>
- /// The <see cref="System.Net.HttpWebRequest"/> method being wrapped
- /// </summary>
- private readonly WebRequest m_request;
- /// <summary>
- /// The current internal state of the object
- /// </summary>
- private RequestStates m_state = RequestStates.Created;
- /// <summary>
- /// The request async wrapper
- /// </summary>
- private AsyncWrapper m_asyncRequest = null;
- /// <summary>
- /// The response async wrapper
- /// </summary>
- private AsyncWrapper m_asyncResponse = null;
- /// <summary>
- /// The request/response timeout value
- /// </summary>
- private int m_timeout = 100000;
- /// <summary>
- /// The activity timeout value
- /// </summary>
- private readonly int m_activity_timeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
-
- /// <summary>
- /// List of valid states
- /// </summary>
- private enum RequestStates
- {
- /// <summary>
- /// The request has been created
- /// </summary>
- Created,
- /// <summary>
- /// The request stream has been requested
- /// </summary>
- GetRequest,
- /// <summary>
- /// The response has been requested
- /// </summary>
- GetResponse,
- /// <summary>
- ///
- /// </summary>
- Done
- }
-
- /// <summary>
- /// Constructs a new request from a url
- /// </summary>
- /// <param name="url">The url to create the request from</param>
- public AsyncHttpRequest(string url)
- : this(System.Net.WebRequest.Create(url))
- {
-
- }
-
- /// <summary>
- /// Creates a async request wrapper for an existing url
- /// </summary>
- /// <param name="request">The request to wrap</param>
- public AsyncHttpRequest(WebRequest request)
- {
- if (request == null)
- throw new ArgumentNullException(nameof(request));
- m_request = request;
- m_timeout = m_request.Timeout;
- if (m_timeout != System.Threading.Timeout.Infinite)
- {
- var tmp = (int)HttpContextSettings.OperationTimeout.TotalMilliseconds;
- if (tmp <= 0)
- m_timeout = System.Threading.Timeout.Infinite;
- else
- m_timeout = Math.Max(m_timeout, tmp);
- }
-
- m_activity_timeout = (int)HttpContextSettings.ReadWriteTimeout.TotalMilliseconds;
- if (m_activity_timeout <= 0)
- m_activity_timeout = System.Threading.Timeout.Infinite;
-
- //We set this to prevent timeout related stuff from happening outside this module
- m_request.Timeout = System.Threading.Timeout.Infinite;
-
- //Then we register custom settings
- if (this.m_request is HttpWebRequest webRequest)
- {
- if (webRequest.ReadWriteTimeout != System.Threading.Timeout.Infinite)
- m_activity_timeout = webRequest.ReadWriteTimeout;
-
- webRequest.ReadWriteTimeout = System.Threading.Timeout.Infinite;
-
- // Prevent in-memory buffering causing out-of-memory issues
- webRequest.AllowReadStreamBuffering = HttpContextSettings.BufferRequests;
- }
- }
-
- /// <summary>
- /// Gets the request that is wrapped
- /// </summary>
- public WebRequest Request { get { return m_request; } }
-
- /// <summary>
- /// Gets or sets the timeout used to guard the <see cref="GetRequestStream(long)"/> and <see cref="GetResponse()"/> calls
- /// </summary>
- public int Timeout { get { return m_timeout; } set { m_timeout = value; } }
-
- /// <summary>
- /// Gets the request stream
- /// </summary>
- /// <returns>The request stream</returns>
- /// <param name="contentlength">The content length to use</param>
- public Stream GetRequestStream(long contentlength = -1)
- {
- // Prevent in-memory buffering causing out-of-memory issues
- if (this.m_request is HttpWebRequest request)
- {
- if (contentlength >= 0)
- request.ContentLength = contentlength;
- if (request.ContentLength >= 0)
- request.AllowWriteStreamBuffering = false;
- }
-
- if (m_state == RequestStates.GetRequest)
- return (Stream)m_asyncRequest.GetResponseOrStream();
-
- if (m_state != RequestStates.Created)
- throw new InvalidOperationException();
-
- m_asyncRequest = new AsyncWrapper(this, true);
- m_state = RequestStates.GetRequest;
-
- return TrySetTimeout((Stream)m_asyncRequest.GetResponseOrStream(), m_activity_timeout);
- }
-
- /// <summary>
- /// Gets the response object
- /// </summary>
- /// <returns>The web response</returns>
- public WebResponse GetResponse()
- {
- if (m_state == RequestStates.GetResponse)
- return (WebResponse)m_asyncResponse.GetResponseOrStream();
-
- if (m_state == RequestStates.Done)
- throw new InvalidOperationException();
-
- m_asyncRequest = null;
- m_asyncResponse = new AsyncWrapper(this, false);
- m_state = RequestStates.GetResponse;
-
- return (WebResponse)m_asyncResponse.GetResponseOrStream();
- }
-
- public Stream GetResponseStream()
- {
- return TrySetTimeout(GetResponse().GetResponseStream(), m_activity_timeout);
- }
-
- public static Stream TrySetTimeout(Stream str, int timeoutmilliseconds = 30000)
- {
- try { str.ReadTimeout = timeoutmilliseconds; }
- catch { }
-
- return str;
- }
-
- /// <summary>
- /// Wrapper class for getting request and response objects in a async manner
- /// </summary>
- private class AsyncWrapper
- {
- private readonly IAsyncResult m_async = null;
- private Stream m_stream = null;
- private WebResponse m_response = null;
- private readonly AsyncHttpRequest m_owner;
- private Exception m_exception = null;
- private readonly ManualResetEvent m_event = new ManualResetEvent(false);
- private readonly bool m_isRequest;
- private bool m_timedout = false;
-
- public AsyncWrapper(AsyncHttpRequest owner, bool isRequest)
- {
- m_owner = owner;
- m_isRequest = isRequest;
-
- if (m_isRequest)
- m_async = m_owner.m_request.BeginGetRequestStream(new AsyncCallback(this.OnAsync), null);
- else
- m_async = m_owner.m_request.BeginGetResponse(new AsyncCallback(this.OnAsync), null);
-
- if ( m_owner.m_timeout != System.Threading.Timeout.Infinite)
- ThreadPool.RegisterWaitForSingleObject(m_async.AsyncWaitHandle, new WaitOrTimerCallback(this.OnTimeout), null, TimeSpan.FromMilliseconds( m_owner.m_timeout), true);
- }
-
- private void OnAsync(IAsyncResult r)
- {
- try
- {
- if (m_isRequest)
- m_stream = m_owner.m_request.EndGetRequestStream(r);
- else
- m_response = m_owner.m_request.EndGetResponse(r);
- }
- catch (Exception ex)
- {
- if (m_timedout)
- m_exception = new WebException(string.Format("{0} timed out", m_isRequest ? "GetRequestStream" : "GetResponse"), ex, WebExceptionStatus.Timeout, ex is WebException exception ? exception.Response : null);
- else
- {
- // Workaround for: https://bugzilla.xamarin.com/show_bug.cgi?id=28287
- var wex = ex;
- if (ex is WebException exception && exception.Response == null)
- {
- WebResponse resp = null;
-
- try { resp = r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(r) as WebResponse; }
- catch {}
-
- if (resp == null)
- 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)
- wex = new WebException(exception.Message, exception.InnerException, exception.Status, resp);
- }
-
- m_exception = wex;
-
-
- }
- }
- finally
- {
- m_event.Set();
- }
- }
-
- private void OnTimeout(object state, bool timedout)
- {
- if (timedout)
- {
- if (!m_event.WaitOne(0, false))
- {
- m_timedout = true;
- m_owner.m_request.Abort();
- }
- }
- }
-
- public object GetResponseOrStream()
- {
- try
- {
- m_event.WaitOne();
- }
- catch (ThreadAbortException)
- {
- m_owner.m_request.Abort();
-
- //Grant a little time for cleanups
- m_event.WaitOne((int)TimeSpan.FromSeconds(5).TotalMilliseconds, false);
-
- //The abort exception will automatically be rethrown
- }
-
- if (m_exception != null)
- throw m_exception;
-
- if (m_isRequest)
- return m_stream;
- else
- return m_response;
- }
- }
- }
-}