Compare commits

...
Author SHA1 Message Date
Kenneth Skovhede fdcda53240 WIP: Make all backends async.
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.
2021-04-17 12:57:35 +02:00
34 changed files with 997 additions and 1418 deletions
@@ -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;
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);
if (!string.IsNullOrEmpty(filename))
{
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
}
try
{
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);
if (!string.IsNullOrEmpty(filename))
{
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
}
foreach (FtpListItem item in ftpClient.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
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()
@@ -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>
@@ -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()
{
@@ -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);
private async Task<List<IListBlobItem>> ListContainerEntriesAsync()
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>> 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 =>
@@ -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>
+93 -103
View File
@@ -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)
{
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 (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
}
);
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;
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()
{
}
@@ -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
{
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;
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);
m_configExpires = DateTime.UtcNow + TimeSpan.FromHours(1);
return m_config;
// Only retry once on client errors
if (ex is HttpRequestStatusException exception)
{
var sc = (int)exception.Response.StatusCode;
clienterror = (sc >= 400 && sc <= 499);
}
}
catch (Exception ex)
catch
{
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))
{
AttemptParseAndThrowException(ex);
throw;
}
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
@@ -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>
+65 -57
View File
@@ -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;
}
private void GetCurrentFolder(bool create)
// 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 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<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
=> this.CondensePaginatedListAsync(cancelToken);
public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
public async Task DeleteAsync(string remotename, 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)
{
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
@@ -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>
@@ -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)
{
var req = CreateRequest("/" + remotename, "");
req.Method = "GET";
public bool SupportsStreaming => true;
var areq = new Utility.AsyncHttpRequest(req);
using (var resp = areq.GetResponse())
using (var s = areq.GetResponseStream())
public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
{
var req = await CreateRequestAsync("/" + remotename, "", cancelToken);
req.Method = HttpMethod.Get;
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);
body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
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;
//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;
}
@@ -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>
@@ -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"); } }
+19 -28
View File
@@ -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 Task<IList<IFileEntry>> ListAsync(CancellationToken cancelToken)
=> this.CondensePaginatedListAsync(cancelToken);
public void Get(string remotename, string filename)
{
using(FileStream fs = File.Create(filename))
Get(remotename, fs);
}
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 void CreateFolder()
public Task TestAsync(CancellationToken cancelToken)
=> this.TestListAsync(cancelToken);
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)
{
@@ -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));
areq = new AsyncHttpRequest(req);
body = new StreamContent(new PartialStream(stream, globalBytesRead, chunksize));
body.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
body.Headers.ContentLength = chunksize;
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; }
@@ -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>
@@ -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>
+52 -49
View File
@@ -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 void CreateFolder()
public Task TestAsync(CancellationToken cancelToken)
=> this.TestListAsync(cancelToken);
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>
+1 -1
View File
@@ -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"); } }
@@ -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>
+56 -34
View File
@@ -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);
+1 -1
View File
@@ -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"); } }
+1 -1
View File
@@ -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
{
@@ -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,43 @@ 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);
if (p.ContentLength >= 0)
sc.Headers.ContentLength = p.ContentLength;
if (!string.IsNullOrWhiteSpace(p.ContentTypeName))
sc.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(p.Headers["Content-Disposition"]);
if (!string.IsNullOrWhiteSpace(p.ContentType))
sc.Headers.ContentType = MediaTypeHeaderValue.Parse(p.ContentType);
mpdata.Add(sc);
}
await rs.WriteAsync(bodyTerminator, 0, bodyTerminator.Length, cancelToken).ConfigureAwait(false);
var req = await CreateRequestAsync(url, "POST", cancelToken);
setup?.Invoke(req);
req.Content = mpdata;
return await m_client.SendAsync(req, cancelToken);
}
return (HttpWebResponse)(await req.GetResponseAsync().ConfigureAwait(false));
}
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;
setup?.Invoke(req);
return req;
}
private static void CreateBoundary(out string boundary, out byte[] bodyterminator)
{
boundary = "----DuplicatiFormBoundary" + Guid.NewGuid().ToString("N");
bodyterminator = Encoding.UTF8.GetBytes("--" + boundary + "--");
}
/// <summary>
/// Executes a web request and json-deserializes the results as the specified type
/// Creates a random form bondary string
/// </summary>
/// <returns>The deserialized JSON data.</returns>
/// <param name="url">The remote URL</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);
}
private static string CreateBoundary()
=> "----DuplicatiFormBoundary" + Guid.NewGuid().ToString("N");
/// <summary>
/// Executes a web request and json-deserializes the results as the specified type
@@ -218,16 +160,24 @@ 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 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
/// </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 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);
return PostAndGetJSONDataAsync<T>(url, requestdata, method, cancelToken);
}
public virtual T ReadJSONResponse<T>(HttpWebRequest req, object requestdata = null)
{
return ReadJSONResponse<T>(new AsyncHttpRequest(req), requestdata);
}
/// <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>(AsyncHttpRequest req, object requestdata = null)
/// <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 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);
}
public virtual T ReadJSONResponse<T>(HttpWebResponse resp)
{
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,52 +291,44 @@ namespace Duplicati.Library
if (method == null && requestdata != null)
method = "POST";
return GetResponseWithoutException(CreateRequest(url, method), requestdata);
return await GetResponseWithoutExceptionAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken);
}
public HttpWebResponse GetResponseWithoutException(HttpWebRequest 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)
{
return GetResponseWithoutException(new AsyncHttpRequest(req), requestdata);
}
public HttpWebResponse GetResponseWithoutException(AsyncHttpRequest req, object requestdata = null)
{
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;
sc.Headers.ContentLength = data.Length;
sc.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=UTF-8");
throw;
req.Content = sc;
}
}
return await m_client.SendAsync(req, cancelToken);
}
public async Task<HttpWebResponse> GetResponseWithoutExceptionAsync(string url, CancellationToken cancelToken, 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");
@@ -379,158 +336,39 @@ namespace Duplicati.Library
if (method == null && requestdata != null)
method = "POST";
return await GetResponseWithoutExceptionAsync(CreateRequest(url, method), cancelToken, requestdata).ConfigureAwait(false);
return await GetResponseAsync(await CreateRequestAsync(url, method, cancelToken), requestdata, cancelToken);
}
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)
public async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage req, object requestdata, CancellationToken cancelToken)
{
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 (WebException wex)
{
if (wex.Response is HttpWebResponse response)
return response;
throw;
}
}
public HttpWebResponse GetResponse(string url, 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 GetResponse(CreateRequest(url, method), requestdata);
}
public HttpWebResponse GetResponse(HttpWebRequest req, object requestdata = null)
{
return GetResponse(new AsyncHttpRequest(req), requestdata);
}
public HttpWebResponse GetResponse(AsyncHttpRequest req, object requestdata = null)
{
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)
public void Dispose()
{
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);
m_client.Dispose();
}
public async Task<HttpWebResponse> GetResponseAsync(AsyncHttpRequest req, CancellationToken cancelToken, object requestdata = null)
public class HttpRequestStatusException : HttpRequestException
{
try
public readonly HttpResponseMessage Response;
public HttpRequestStatusException(HttpResponseMessage resp)
: base(resp.ReasonPhrase)
{
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;
}
}
protected class HeaderPart
{
public readonly byte[] Header;
public readonly MultipartItem Part;
public HeaderPart(byte[] header, MultipartItem part)
{
Header = header;
Part = part;
Response = resp;
}
}
@@ -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
@@ -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);
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
retries++;
}
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++;
}
}
return m_token;
}
return m_token;
}
public void ThrowOverQuotaError()
@@ -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);
}
}
}
+13 -10
View File
@@ -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;
}
}
}
}
}
@@ -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;
}
}
}
@@ -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>
+24 -8
View File
@@ -19,6 +19,7 @@
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -46,32 +47,43 @@ namespace Duplicati.Library.Interface
/// </summary>
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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
}
}