Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdcda53240 |
@@ -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()
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"); } }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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"); } }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "folder",
|
||||
/// 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 "folder".
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user