Merge pull request #6065 from marceloduplicati/feature/box-http-rework
Terminal Box.com replacement of deprecated WebClient/OAuth
This commit is contained in:
@@ -19,24 +19,28 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Uri = Duplicati.Library.Utility.Uri;
|
||||
|
||||
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 : IStreamingBackend
|
||||
{
|
||||
private static readonly string LOGTAG = Logging.Log.LogTagFromType<BoxBackend>();
|
||||
private static readonly string LOGTAG = Log.LogTagFromType<BoxBackend>();
|
||||
|
||||
private const string AUTHID_OPTION = "authid";
|
||||
private const string REALLY_DELETE_OPTION = "box-delete-from-trash";
|
||||
@@ -46,14 +50,14 @@ namespace Duplicati.Library.Backend.Box
|
||||
|
||||
private const int PAGE_SIZE = 200;
|
||||
|
||||
private readonly BoxHelper m_oauth;
|
||||
private readonly string m_path;
|
||||
private readonly bool m_deleteFromTrash;
|
||||
private readonly BoxHelper _oAuthHelper;
|
||||
private readonly string _path;
|
||||
private readonly bool _deleteFromTrash;
|
||||
|
||||
private string m_currentfolder;
|
||||
private readonly Dictionary<string, string> m_filecache = new Dictionary<string, string>();
|
||||
private string _currentFolder;
|
||||
private readonly Dictionary<string, string> _fileCache = new();
|
||||
|
||||
private class BoxHelper : OAuthHelper
|
||||
private class BoxHelper : OAuthHelperHttpClient
|
||||
{
|
||||
public BoxHelper(string authid)
|
||||
: base(authid, "box.com")
|
||||
@@ -61,108 +65,94 @@ namespace Duplicati.Library.Backend.Box
|
||||
AutoAuthHeader = true;
|
||||
}
|
||||
|
||||
protected override void ParseException(Exception ex)
|
||||
public override void AttemptParseAndThrowException(Exception ex, HttpResponseMessage responseContext = null)
|
||||
{
|
||||
Exception newex = null;
|
||||
try
|
||||
{
|
||||
if (ex is WebException exception && exception.Response is HttpWebResponse hs)
|
||||
{
|
||||
string rawdata = null;
|
||||
using (var rs = Library.Utility.AsyncHttpRequest.TrySetTimeout(hs.GetResponseStream()))
|
||||
using (var sr = new System.IO.StreamReader(rs))
|
||||
rawdata = sr.ReadToEnd();
|
||||
AttemptParseAndThrowExceptionAsync(ex, responseContext).Await();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawdata))
|
||||
return;
|
||||
public override async Task AttemptParseAndThrowExceptionAsync(Exception ex, HttpResponseMessage responseContext = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (ex is not HttpRequestException || responseContext == null)
|
||||
return;
|
||||
|
||||
if (responseContext is { StatusCode: HttpStatusCode.TooManyRequests })
|
||||
throw new TooManyRequestException(responseContext.Headers.RetryAfter);
|
||||
|
||||
newex = new Exception("Raw message: " + rawdata);
|
||||
|
||||
var msg = JsonConvert.DeserializeObject<ErrorResponse>(rawdata);
|
||||
newex = new Exception(string.Format("{0} - {1}: {2}", msg.Status, msg.Code, msg.Message));
|
||||
|
||||
/*if (msg.ContextInfo != null && msg.ContextInfo.Length > 0)
|
||||
newex = new Exception(string.Format("{0} - {1}: {2}{3}{4}", msg.Status, msg.Code, msg.Message, Environment.NewLine, string.Join("; ", from n in msg.ContextInfo select n.Message)));
|
||||
*/
|
||||
}
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
Library.Logging.Log.WriteWarningMessage(LOGTAG, "BoxErrorParser", ex2, "Failed to parse error from Box");
|
||||
}
|
||||
|
||||
if (newex != null)
|
||||
throw newex;
|
||||
await using var stream = await responseContext.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
var rawData = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(rawData);
|
||||
throw new UserInformationException($"Box.com ErrorResponse: {errorResponse.Status} - {errorResponse.Code}: {errorResponse.Message}", "box.com");
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
public BoxBackend()
|
||||
{
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
// This constructor is needed by the BackendLoader.
|
||||
|
||||
public BoxBackend(string url, Dictionary<string, string> options)
|
||||
{
|
||||
var uri = new Utility.Uri(url);
|
||||
var uri = new Uri(url);
|
||||
|
||||
m_path = Util.AppendDirSeparator(uri.HostAndPath, "/");
|
||||
_path = Util.AppendDirSeparator(uri.HostAndPath, "/");
|
||||
|
||||
string authid = null;
|
||||
if (options.ContainsKey(AUTHID_OPTION))
|
||||
authid = options[AUTHID_OPTION];
|
||||
if (options.TryGetValue(AUTHID_OPTION, out var option))
|
||||
authid = option;
|
||||
|
||||
m_deleteFromTrash = Library.Utility.Utility.ParseBoolOption(options, REALLY_DELETE_OPTION);
|
||||
_deleteFromTrash = Utility.Utility.ParseBoolOption(options, REALLY_DELETE_OPTION);
|
||||
|
||||
m_oauth = new BoxHelper(authid);
|
||||
_oAuthHelper = new BoxHelper(authid);
|
||||
}
|
||||
|
||||
private async Task<string> GetCurrentFolderWithCacheAsync(CancellationToken cancelToken)
|
||||
{
|
||||
if (m_currentfolder == null)
|
||||
if (_currentFolder == null)
|
||||
await GetCurrentFolderAsync(false, cancelToken).ConfigureAwait(false);
|
||||
|
||||
return m_currentfolder;
|
||||
return _currentFolder;
|
||||
}
|
||||
|
||||
private async Task GetCurrentFolderAsync(bool create, CancellationToken cancelToken)
|
||||
{
|
||||
var parentid = "0";
|
||||
|
||||
foreach (var p in m_path.Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var p in _path.Split(["/"], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var el = (MiniFolder)await PagedFileListResponse(parentid, true, cancelToken).FirstOrDefaultAsync(x => x.Name == p).ConfigureAwait(false);
|
||||
var el = (MiniFolder)await PagedFileListResponse(parentid, true, cancelToken).FirstOrDefaultAsync(x => x.Name == p, cancellationToken: cancelToken).ConfigureAwait(false);
|
||||
if (el == null)
|
||||
{
|
||||
if (!create)
|
||||
throw new FolderMissingException();
|
||||
|
||||
el = await m_oauth.PostAndGetJSONDataAsync<ListFolderResponse>(
|
||||
string.Format("{0}/folders", BOX_API_URL),
|
||||
cancelToken,
|
||||
new CreateItemRequest() { Name = p, Parent = new IDReference() { ID = parentid } }
|
||||
|
||||
el = await _oAuthHelper.PostAndGetJsonDataAsync<ListFolderResponse>(
|
||||
$"{BOX_API_URL}/folders",
|
||||
new CreateItemRequest
|
||||
{
|
||||
Name = p, Parent = new IDReference { ID = parentid }
|
||||
},
|
||||
cancelToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
parentid = el.ID;
|
||||
}
|
||||
|
||||
m_currentfolder = parentid;
|
||||
_currentFolder = parentid;
|
||||
}
|
||||
|
||||
private async Task<string> GetFileIDAsync(string name, CancellationToken cancelToken)
|
||||
private async Task<string> GetFileIdAsync(string name, CancellationToken cancelToken)
|
||||
{
|
||||
if (m_filecache.ContainsKey(name))
|
||||
return m_filecache[name];
|
||||
if (_fileCache.TryGetValue(name, out var async))
|
||||
return async;
|
||||
|
||||
// Make sure we enumerate this, otherwise the m_filecache is empty.
|
||||
var currentFolder = await GetCurrentFolderWithCacheAsync(cancelToken).ConfigureAwait(false);
|
||||
await PagedFileListResponse(currentFolder, false, cancelToken).LastOrDefaultAsync().ConfigureAwait(false);
|
||||
await PagedFileListResponse(currentFolder, false, cancelToken).LastOrDefaultAsync(cancellationToken: cancelToken).ConfigureAwait(false);
|
||||
|
||||
if (m_filecache.ContainsKey(name))
|
||||
return m_filecache[name];
|
||||
if (_fileCache.TryGetValue(name, out var idAsync))
|
||||
return idAsync;
|
||||
|
||||
throw new FileMissingException();
|
||||
}
|
||||
@@ -173,11 +163,11 @@ namespace Duplicati.Library.Backend.Box
|
||||
var done = false;
|
||||
|
||||
if (!onlyfolders)
|
||||
m_filecache.Clear();
|
||||
_fileCache.Clear();
|
||||
|
||||
do
|
||||
{
|
||||
var resp = await m_oauth.GetJSONDataAsync<ShortListResponse>($"{BOX_API_URL}/folders/{parentid}/items?limit={PAGE_SIZE}&offset={offset}&fields=name,size,modified_at", cancelToken).ConfigureAwait(false);
|
||||
var resp = await _oAuthHelper.GetJsonDataAsync<ShortListResponse>($"{BOX_API_URL}/folders/{parentid}/items?limit={PAGE_SIZE}&offset={offset}&fields=name,size,modified_at", cancelToken).ConfigureAwait(false);
|
||||
|
||||
if (resp.Entries == null || resp.Entries.Length == 0)
|
||||
break;
|
||||
@@ -189,13 +179,11 @@ namespace Duplicati.Library.Backend.Box
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!onlyfolders && f.Type == "file")
|
||||
m_filecache[f.Name] = f.ID;
|
||||
|
||||
yield return f;
|
||||
}
|
||||
if (!onlyfolders && f.Type == "file")
|
||||
_fileCache[f.Name] = f.ID;
|
||||
|
||||
yield return f;
|
||||
}
|
||||
|
||||
offset = offset + PAGE_SIZE;
|
||||
@@ -206,62 +194,59 @@ namespace Duplicati.Library.Backend.Box
|
||||
} while (!done);
|
||||
}
|
||||
|
||||
#region IStreamingBackend implementation
|
||||
|
||||
public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
|
||||
public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
|
||||
{
|
||||
var currentFolder = await GetCurrentFolderWithCacheAsync(cancelToken).ConfigureAwait(false);
|
||||
var createreq = new CreateItemRequest()
|
||||
var createreq = new CreateItemRequest
|
||||
{
|
||||
Name = remotename,
|
||||
Parent = new IDReference()
|
||||
Parent = new IDReference
|
||||
{
|
||||
ID = currentFolder
|
||||
}
|
||||
};
|
||||
|
||||
if (m_filecache.Count == 0)
|
||||
await PagedFileListResponse(currentFolder, false, cancelToken).LastOrDefaultAsync().ConfigureAwait(false);
|
||||
if (_fileCache.Count == 0)
|
||||
await PagedFileListResponse(currentFolder, false, cancelToken).LastOrDefaultAsync(cancelToken).ConfigureAwait(false);
|
||||
|
||||
var existing = m_filecache.ContainsKey(remotename);
|
||||
var existing = _fileCache.ContainsKey(remotename);
|
||||
|
||||
var multipartForm = new MultipartFormDataContent();
|
||||
|
||||
try
|
||||
{
|
||||
string url;
|
||||
var items = new List<MultipartItem>(2);
|
||||
|
||||
if (existing)
|
||||
url = $"{BOX_UPLOAD_URL}/{m_filecache[remotename]}/content";
|
||||
url = $"{BOX_UPLOAD_URL}/{_fileCache[remotename]}/content";
|
||||
else
|
||||
{
|
||||
url = $"{BOX_UPLOAD_URL}/content";
|
||||
items.Add(new MultipartItem(createreq, "attributes"));
|
||||
multipartForm.Add(JsonContent.Create(createreq),"attributes");
|
||||
}
|
||||
|
||||
items.Add(new MultipartItem(stream, "file", remotename));
|
||||
|
||||
var res = (await m_oauth.PostMultipartAndGetJSONDataAsync<FileList>(url, null, cancelToken, items.ToArray())).Entries.First();
|
||||
m_filecache[remotename] = res.ID;
|
||||
|
||||
multipartForm.Add(new StreamContent(stream), "file", remotename);
|
||||
|
||||
var res = (await _oAuthHelper.PostMultipartAndGetJsonDataAsync<FileList>(url, cancelToken, multipartForm)).Entries.First();
|
||||
_fileCache[remotename] = res.ID;
|
||||
}
|
||||
catch
|
||||
{
|
||||
m_filecache.Clear();
|
||||
_fileCache.Clear();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GetAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken)
|
||||
public async Task GetAsync(string remotename, Stream stream, CancellationToken cancelToken)
|
||||
{
|
||||
var fileId = await GetFileIDAsync(remotename, cancelToken).ConfigureAwait(false);
|
||||
using (var resp = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}/content", BOX_API_URL, fileId), cancelToken).ConfigureAwait(false))
|
||||
using (var rs = Duplicati.Library.Utility.AsyncHttpRequest.TrySetTimeout(resp.GetResponseStream()))
|
||||
await Library.Utility.Utility.CopyStreamAsync(rs, stream, cancelToken).ConfigureAwait(false);
|
||||
var fileId = await GetFileIdAsync(remotename, cancelToken).ConfigureAwait(false);
|
||||
using var request = _oAuthHelper.CreateRequest($"{BOX_API_URL}/files/{fileId}/content");
|
||||
using var resp = await _oAuthHelper.GetResponseAsync(request, cancelToken).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
await using var responseStream = await resp.Content.ReadAsStreamAsync(cancelToken).ConfigureAwait(false);
|
||||
await Utility.Utility.CopyStreamAsync(responseStream, stream, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IBackend implementation
|
||||
|
||||
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancelToken)
|
||||
{
|
||||
var currentFolder = await GetCurrentFolderWithCacheAsync(cancelToken).ConfigureAwait(false);
|
||||
@@ -271,33 +256,38 @@ namespace Duplicati.Library.Backend.Box
|
||||
|
||||
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
|
||||
{
|
||||
using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
|
||||
await PutAsync(remotename, fs, cancelToken);
|
||||
await using FileStream fs = File.OpenRead(filename);
|
||||
await PutAsync(remotename, fs, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken)
|
||||
{
|
||||
using (System.IO.FileStream fs = System.IO.File.Create(filename))
|
||||
await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false);
|
||||
await using FileStream fs = File.Create(filename);
|
||||
await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
|
||||
{
|
||||
var fileid = await GetFileIDAsync(remotename, cancelToken).ConfigureAwait(false);
|
||||
var fileId = await GetFileIdAsync(remotename, cancelToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
using (var r = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}", BOX_API_URL, fileid), cancelToken, null, "DELETE").ConfigureAwait(false))
|
||||
using (var request = _oAuthHelper.CreateRequest($"{BOX_API_URL}/files/{fileId}","DELETE"))
|
||||
using (var r = await _oAuthHelper.GetResponseAsync(request, cancelToken).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
|
||||
if (m_deleteFromTrash)
|
||||
using (var r = await m_oauth.GetResponseAsync(string.Format("{0}/files/{1}/trash", BOX_API_URL, fileid), cancelToken, null, "DELETE").ConfigureAwait(false))
|
||||
if (_deleteFromTrash)
|
||||
{
|
||||
using var request = _oAuthHelper.CreateRequest($"{BOX_API_URL}/files/{fileId}/trash","DELETE");
|
||||
using (var r = await _oAuthHelper
|
||||
.GetResponseAsync(request, cancelToken).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
m_filecache.Clear();
|
||||
_fileCache.Clear();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -310,202 +300,26 @@ namespace Duplicati.Library.Backend.Box
|
||||
return GetCurrentFolderAsync(true, cancellationToken);
|
||||
}
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
{
|
||||
return Strings.Box.DisplayName;
|
||||
}
|
||||
}
|
||||
public string DisplayName => Strings.Box.DisplayName;
|
||||
|
||||
public string ProtocolKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return "box";
|
||||
}
|
||||
}
|
||||
public string ProtocolKey => "box";
|
||||
|
||||
public IList<ICommandLineArgument> SupportedCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
|
||||
new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Box.AuthidShort, Strings.Box.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("box.com"))),
|
||||
new CommandLineArgument(REALLY_DELETE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Box.ReallydeleteShort, Strings.Box.ReallydeleteLong),
|
||||
});
|
||||
}
|
||||
}
|
||||
public IList<ICommandLineArgument> SupportedCommands =>
|
||||
new List<ICommandLineArgument>([
|
||||
new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Box.AuthidShort, Strings.Box.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("box.com"))),
|
||||
new CommandLineArgument(REALLY_DELETE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Box.ReallydeleteShort, Strings.Box.ReallydeleteLong)
|
||||
]);
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
return Strings.Box.Description;
|
||||
}
|
||||
}
|
||||
public string Description => Strings.Box.Description;
|
||||
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new string[] {
|
||||
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] {
|
||||
new System.Uri(BOX_API_URL).Host,
|
||||
new System.Uri(BOX_UPLOAD_URL).Host
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private class MiniUser : IDReference
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("login")]
|
||||
public string Login { get; set; }
|
||||
}
|
||||
|
||||
private class MiniFolder : IDReference
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("etag")]
|
||||
public string ETag { get; set; }
|
||||
[JsonProperty("sequence_id")]
|
||||
public string SequenceID { get; set; }
|
||||
}
|
||||
|
||||
private class FileEntity : MiniFolder
|
||||
{
|
||||
public FileEntity() { Size = -1; }
|
||||
|
||||
[JsonProperty("sha1")]
|
||||
public string SHA1 { get; set; }
|
||||
|
||||
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public long Size { get; set; }
|
||||
[JsonProperty("modified_at", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
}
|
||||
|
||||
private class FolderList
|
||||
{
|
||||
[JsonProperty("total_count")]
|
||||
public long TotalCount { get; set; }
|
||||
[JsonProperty("entries")]
|
||||
public MiniFolder[] Entries { get; set; }
|
||||
}
|
||||
|
||||
private class FileList
|
||||
{
|
||||
[JsonProperty("total_count")]
|
||||
public long TotalCount { get; set; }
|
||||
[JsonProperty("entries")]
|
||||
public FileEntity[] Entries { get; set; }
|
||||
[JsonProperty("offset")]
|
||||
public long Offset { get; set; }
|
||||
[JsonProperty("limit")]
|
||||
public long Limit { get; set; }
|
||||
}
|
||||
|
||||
private class UploadEmail
|
||||
{
|
||||
[JsonProperty("access")]
|
||||
public string Access { get; set; }
|
||||
[JsonProperty("email")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
private class ListFolderResponse : MiniFolder
|
||||
{
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
[JsonProperty("modified_at")]
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
[JsonProperty("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("path_collection")]
|
||||
public FolderList PathCollection { get; set; }
|
||||
|
||||
[JsonProperty("created_by")]
|
||||
public MiniUser CreatedBy { get; set; }
|
||||
[JsonProperty("modified_by")]
|
||||
public MiniUser ModifiedBy { get; set; }
|
||||
[JsonProperty("owned_by")]
|
||||
public MiniUser OwnedBy { get; set; }
|
||||
|
||||
[JsonProperty("shared_link")]
|
||||
public MiniUser SharedLink { get; set; }
|
||||
|
||||
[JsonProperty("folder_upload_email")]
|
||||
public UploadEmail FolderUploadEmail { get; set; }
|
||||
|
||||
[JsonProperty("parent")]
|
||||
public MiniFolder Parent { get; set; }
|
||||
|
||||
[JsonProperty("item_status")]
|
||||
public string ItemStatus { get; set; }
|
||||
|
||||
[JsonProperty("item_collection")]
|
||||
public FileList ItemCollection { get; set; }
|
||||
|
||||
}
|
||||
|
||||
private class OrderEntry
|
||||
{
|
||||
[JsonProperty("by")]
|
||||
public string By { get; set; }
|
||||
[JsonProperty("direction")]
|
||||
public string Direction { get; set; }
|
||||
}
|
||||
|
||||
private class ShortListResponse : FileList
|
||||
{
|
||||
[JsonProperty("order")]
|
||||
public OrderEntry[] Order { get; set; }
|
||||
|
||||
}
|
||||
|
||||
private class IDReference
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string ID { get; set; }
|
||||
}
|
||||
|
||||
private class CreateItemRequest
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("parent")]
|
||||
public IDReference Parent { get; set; }
|
||||
}
|
||||
|
||||
private class ErrorResponse
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
[JsonProperty("code")]
|
||||
public string Code { get; set; }
|
||||
[JsonProperty("help_url")]
|
||||
public string HelpUrl { get; set; }
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
[JsonProperty("request_id")]
|
||||
public string RequestId { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class CreateItemRequest
|
||||
{
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("parent")] public IDReference Parent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class ErrorResponse
|
||||
{
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("status")] public int Status { get; set; }
|
||||
[JsonProperty("code")] public string Code { get; set; }
|
||||
[JsonProperty("help_url")] public string HelpUrl { get; set; }
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
[JsonProperty("request_id")] public string RequestId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class FileEntity : MiniFolder
|
||||
{
|
||||
public FileEntity()
|
||||
{
|
||||
Size = -1;
|
||||
}
|
||||
|
||||
[JsonProperty("sha1")] public string SHA1 { get; set; }
|
||||
|
||||
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("modified_at", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class FileList
|
||||
{
|
||||
[JsonProperty("total_count")] public long TotalCount { get; set; }
|
||||
[JsonProperty("entries")] public FileEntity[] Entries { get; set; }
|
||||
[JsonProperty("offset")] public long Offset { get; set; }
|
||||
[JsonProperty("limit")] public long Limit { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class FolderList
|
||||
{
|
||||
[JsonProperty("total_count")] public long TotalCount { get; set; }
|
||||
[JsonProperty("entries")] public MiniFolder[] Entries { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class IDReference
|
||||
{
|
||||
[JsonProperty("id")] public string ID { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class ListFolderResponse : MiniFolder
|
||||
{
|
||||
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
|
||||
[JsonProperty("modified_at")] public DateTime ModifiedAt { get; set; }
|
||||
[JsonProperty("description")] public string Description { get; set; }
|
||||
[JsonProperty("size")] public long Size { get; set; }
|
||||
[JsonProperty("path_collection")] public FolderList PathCollection { get; set; }
|
||||
[JsonProperty("created_by")] public MiniUser CreatedBy { get; set; }
|
||||
[JsonProperty("modified_by")] public MiniUser ModifiedBy { get; set; }
|
||||
[JsonProperty("owned_by")] public MiniUser OwnedBy { get; set; }
|
||||
[JsonProperty("shared_link")] public MiniUser SharedLink { get; set; }
|
||||
[JsonProperty("folder_upload_email")] public UploadEmail FolderUploadEmail { get; set; }
|
||||
[JsonProperty("parent")] public MiniFolder Parent { get; set; }
|
||||
[JsonProperty("item_status")] public string ItemStatus { get; set; }
|
||||
[JsonProperty("item_collection")] public FileList ItemCollection { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class MiniFolder : IDReference
|
||||
{
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("etag")] public string ETag { get; set; }
|
||||
[JsonProperty("sequence_id")] public string SequenceID { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class MiniUser : IDReference
|
||||
{
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("login")] public string Login { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class OrderEntry
|
||||
{
|
||||
[JsonProperty("by")] public string By { get; set; }
|
||||
[JsonProperty("direction")] public string Direction { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class ShortListResponse : FileList
|
||||
{
|
||||
[JsonProperty("order")] public OrderEntry[] Order { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Duplicati.Library.Backend.Box;
|
||||
|
||||
public class UploadEmail
|
||||
{
|
||||
[JsonProperty("access")] public string Access { get; set; }
|
||||
[JsonProperty("email")] public string Email { get; set; }
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Duplicati.Library;
|
||||
public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpClient reference
|
||||
/// HttpClient reference for inheritors
|
||||
/// </summary>
|
||||
protected readonly HttpClient _httpClient = httpClient;
|
||||
|
||||
@@ -62,7 +62,36 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <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="cancellationToken">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, CancellationToken cancellationToken, MultipartContent parts)
|
||||
{
|
||||
using var response = await PostMultipartAsync(url, cancellationToken, parts).ConfigureAwait(false);
|
||||
return ReadJsonResponse<T>(response);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Performs a multipart post
|
||||
/// </summary>
|
||||
/// <returns>The response.</returns>
|
||||
/// <param name="url">The url to post to.</param>
|
||||
/// <param name="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <param name="parts">The multipart items.</param>
|
||||
protected virtual async Task<HttpResponseMessage> PostMultipartAsync(string url, CancellationToken cancellationToken, MultipartContent parts)
|
||||
{
|
||||
using var req = CreateRequest(url, "POST");
|
||||
req.Content = parts;
|
||||
return await _httpClient.SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute Get request and return response and deserializes JSON response into the given type
|
||||
/// </summary>
|
||||
@@ -72,7 +101,7 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
/// <typeparam name="T">Destination Type</typeparam>
|
||||
protected virtual async Task<T> GetJsonDataAsync<T>(string url, CancellationToken cancellationToken, Action<HttpRequestMessage> setup = null)
|
||||
{
|
||||
var req = CreateRequest(url);
|
||||
using var req = CreateRequest(url);
|
||||
|
||||
if (setup != null)
|
||||
setup(req);
|
||||
@@ -103,6 +132,26 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <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="cancellationToken">Token to cancel the operation.</param>
|
||||
/// <typeparam name="T">The type of data to return.</typeparam>
|
||||
public virtual async Task<T> GetJsonDataAsync<T>(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
return await GetJsonDataAsync<T>(
|
||||
url,
|
||||
cancellationToken,
|
||||
request =>
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the JSON response from the server and deserializes it into the given type
|
||||
@@ -113,7 +162,7 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
protected virtual async Task<T> ReadJsonResponseAsync<T>(HttpRequestMessage req, CancellationToken cancellationToken)
|
||||
{
|
||||
using var resp = await GetResponseAsync(req, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadJsonResponsAsync<T>(resp, cancellationToken).ConfigureAwait(false);
|
||||
return await ReadJsonResponseAsync<T>(resp, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -151,7 +200,7 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
/// <typeparam name="T">Type to cast to</typeparam>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="IOException">Exception when failing to deserialize the JSON to the Type</exception>
|
||||
protected virtual async Task<T> ReadJsonResponsAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
protected virtual async Task<T> ReadJsonResponseAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var rs = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var ps = new StreamPeekReader(rs);
|
||||
@@ -171,13 +220,22 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to register an exception handler,
|
||||
/// which can throw another, more meaningful exception
|
||||
/// </summary>
|
||||
public virtual async Task AttemptParseAndThrowExceptionAsync(Exception ex, HttpResponseMessage responseContext = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to register an exception handler,
|
||||
/// which can throw another, more meaningful exception
|
||||
/// </summary>
|
||||
public virtual void AttemptParseAndThrowException(Exception ex, HttpResponseMessage responseContext = null)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,13 +244,14 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
/// <param name="req">Request object</param>
|
||||
/// <param name="cancellationToken">Cancellation Token</param>
|
||||
/// <returns></returns>
|
||||
private async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage req, CancellationToken cancellationToken)
|
||||
public async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage req, CancellationToken cancellationToken)
|
||||
{
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -277,6 +336,5 @@ public class JsonWebHelperHttpClient(HttpClient httpClient)
|
||||
|
||||
return source.Read(buffer, offset, count) + br;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.Library;
|
||||
|
||||
public class OAuthHelperHttpClient : JsonWebHelperHttpClient
|
||||
{
|
||||
private string _Token;
|
||||
private string _Authid;
|
||||
private DateTime _mTokenExpires = DateTime.UtcNow;
|
||||
private string OAuthLoginUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for authentication requests
|
||||
/// </summary>
|
||||
private const int AUTHENTICATION_TIMEOUT_SECONDS = 25;
|
||||
|
||||
private const int MAX_AUTHORIZATION_RETRIES = 5;
|
||||
|
||||
private static string OAUTH_LOGIN_URL(string modulename)
|
||||
{
|
||||
var u = new Utility.Uri(OAuthContextSettings.ServerURL);
|
||||
var addr = u.SetPath("")
|
||||
.SetQuery((u.Query ?? "") + (string.IsNullOrWhiteSpace(u.Query) ? "" : "&") + "type={0}");
|
||||
return string.Format(addr.ToString(), modulename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to automatically add the Authorization header to requests
|
||||
/// </summary>
|
||||
public bool AutoAuthHeader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the provider does not use refresh tokens, but only access tokens
|
||||
/// </summary>
|
||||
public bool AccessTokenOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true (the default), when a v1 authid is being used it will be swapped
|
||||
/// with a v2 authid, when the OAuth service returns one (which it dypically
|
||||
/// does after a provider token refresh has been performed). Some providers
|
||||
/// are not compatible with v2 authid, tyically because they generate a new
|
||||
/// refresh token with every access token refresh and invalidates the old.
|
||||
/// If the oauth service still returns a v2 authid for such a provider,
|
||||
/// set this property to false to make Duplicati ignore it.
|
||||
/// </summary>
|
||||
public bool AutoV2 { get; set; } = true;
|
||||
|
||||
public OAuthHelperHttpClient(string authid, string servicename, HttpClient httpClient = null, string useragent = null)
|
||||
: base(httpClient ?? HttpClientHelper.CreateClient())
|
||||
{
|
||||
_Authid = authid;
|
||||
OAuthLoginUrl = OAUTH_LOGIN_URL(servicename);
|
||||
|
||||
if (string.IsNullOrEmpty(authid))
|
||||
throw new Interface.UserInformationException(
|
||||
Strings.OAuthHelper.MissingAuthID(OAuthLoginUrl), "MissingAuthID");
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(string url, string method, bool noAuthorization, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new HttpRequestMessage(string.IsNullOrEmpty(method) ? HttpMethod.Get : new HttpMethod(method), url);
|
||||
request.Headers.Add("User-Agent", UserAgent);
|
||||
if (!noAuthorization && AutoAuthHeader && !string.Equals(OAuthContextSettings.ServerURL, url)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
|
||||
GetAccessTokenAsync(cancellationToken).Await());
|
||||
return request;
|
||||
}
|
||||
|
||||
public override HttpRequestMessage CreateRequest(string url, string method = null)
|
||||
{
|
||||
return CreateRequest(url, method, false);
|
||||
}
|
||||
|
||||
private async Task <string> GetAccessTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (AccessTokenOnly)
|
||||
return _Authid;
|
||||
|
||||
if (_Token == null || _mTokenExpires < DateTime.UtcNow)
|
||||
{
|
||||
var retries = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
using var timeoutToken = new CancellationTokenSource();
|
||||
timeoutToken.CancelAfter(TimeSpan.FromSeconds(AUTHENTICATION_TIMEOUT_SECONDS));
|
||||
using var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
|
||||
|
||||
using var request = CreateRequest(OAuthContextSettings.ServerURL, "GET", false, combinedCancellationToken.Token);
|
||||
|
||||
request.Headers.TryAddWithoutValidation("X-AuthID", _Authid);
|
||||
response = await _httpClient.SendAsync(request, combinedCancellationToken.Token).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var res = await ReadJsonResponseAsync<OAuthServiceResponse>(response, combinedCancellationToken.Token).ConfigureAwait(false);
|
||||
|
||||
_mTokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30);
|
||||
if (AutoV2 && !string.IsNullOrWhiteSpace(res.v2_authid))
|
||||
_Authid = res.v2_authid;
|
||||
return _Token = res.access_token;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var clientError = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Only retry once on client errors
|
||||
if (ex is HttpRequestException { StatusCode: not null } exception)
|
||||
{
|
||||
var sc = (int)exception.StatusCode;
|
||||
clientError = sc is >= 400 and <= 499;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (response != null && response.Headers.Contains("X-Reason"))
|
||||
{
|
||||
var msg = response.Headers.GetValues("X-Reason").FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(msg))
|
||||
msg = response.StatusCode.ToString();
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
string errorKey = msg == response.StatusCode.ToString()
|
||||
? "OAuthOverQuotaError"
|
||||
: "OAuthLoginError";
|
||||
|
||||
string errorMessage = errorKey == "OAuthOverQuotaError"
|
||||
? Strings.OAuthHelper.OverQuotaError
|
||||
: Strings.OAuthHelper.AuthorizationFailure(msg, OAuthLoginUrl);
|
||||
|
||||
throw new Interface.UserInformationException(errorMessage, errorKey,
|
||||
errorKey == "OAuthLoginError" ? ex : null);
|
||||
}
|
||||
}
|
||||
|
||||
if (retries >= (clientError ? 1 : MAX_AUTHORIZATION_RETRIES))
|
||||
{
|
||||
await AttemptParseAndThrowExceptionAsync(ex, response, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries)));
|
||||
retries++;
|
||||
}
|
||||
finally
|
||||
{
|
||||
response?.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return _Token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2025, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
namespace Duplicati.Library;
|
||||
|
||||
public class OAuthServiceResponse
|
||||
{
|
||||
public string access_token { get; set; }
|
||||
|
||||
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
public int expires { get; set; }
|
||||
|
||||
[Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
|
||||
public string v2_authid { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user