Files
duplicati/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs
T

417 lines
16 KiB
C#
Raw Normal View History

2025-03-20 10:11:30 +01:00
// 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.
2025-03-21 10:49:36 +01:00
2024-02-28 15:45:30 +01:00
using Duplicati.Library.Utility;
2025-03-21 10:49:36 +01:00
using Duplicati.Library.Utility.Options;
2016-07-07 13:44:19 +02:00
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net;
2016-07-07 13:44:19 +02:00
namespace Duplicati.Library.Backend
{
public class DropboxHelper : OAuthHelper
2016-07-07 13:44:19 +02:00
{
private const int DROPBOX_MAX_CHUNK_UPLOAD = 10 * 1024 * 1024; // 10 MB max upload
2016-07-07 13:44:19 +02:00
private const string API_ARG_HEADER = "DROPBOX-API-arg";
2025-03-21 10:49:36 +01:00
private readonly TimeoutOptionsHelper.Timeouts m_timeouts;
public DropboxHelper(string accessToken, TimeoutOptionsHelper.Timeouts timeouts)
: base(accessToken, "dropbox")
2016-07-07 13:44:19 +02:00
{
2025-03-21 10:49:36 +01:00
m_timeouts = timeouts;
base.AutoAuthHeader = true;
2022-02-23 21:58:04 +01:00
// Pre 2022 tokens are direct Dropbox tokens (no ':')
// Post 2022-02-21 tokens are regular authid tokens (with a ':')
base.AccessTokenOnly = !accessToken.Contains(":");
2016-07-07 13:44:19 +02:00
}
2025-02-17 16:45:51 +01:00
public async Task<ListFolderResult> ListFiles(string path, CancellationToken cancelToken)
2016-07-07 13:44:19 +02:00
{
2019-04-28 17:02:46 -07:00
var pa = new PathArg
{
path = path
2018-10-23 15:46:35 +02:00
};
2016-07-07 13:44:19 +02:00
try
{
2025-03-21 10:49:36 +01:00
return await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, ct =>
PostAndGetJSONDataAsync<ListFolderResult>(WebApi.Dropbox.ListFilesUrl(), ct, pa)
).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, false);
2016-07-07 13:44:19 +02:00
throw;
}
}
2025-02-17 16:45:51 +01:00
public async Task<ListFolderResult> ListFilesContinue(string cursor, CancellationToken cancelToken)
2016-07-07 13:44:19 +02:00
{
var lfca = new ListFolderContinueArg() { cursor = cursor };
2016-07-07 13:44:19 +02:00
try
{
2025-03-21 10:49:36 +01:00
return await Utility.Utility.WithTimeout(m_timeouts.ListTimeout, cancelToken, ct =>
PostAndGetJSONDataAsync<ListFolderResult>(WebApi.Dropbox.ListFilesContinueUrl(), ct, lfca)
).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, false);
2016-07-07 13:44:19 +02:00
throw;
}
}
public async Task<FolderMetadata> CreateFolderAsync(string path, CancellationToken cancellationToken)
2016-07-07 13:44:19 +02:00
{
var pa = new PathArg() { path = path };
2016-07-07 13:44:19 +02:00
try
{
2025-03-21 10:49:36 +01:00
return await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancellationToken, ct =>
PostAndGetJSONDataAsync<FolderMetadata>(WebApi.Dropbox.CreateFolderUrl(), ct, pa)
).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, false);
2016-07-07 13:44:19 +02:00
throw;
}
}
public async Task<FileMetaData> UploadFileAsync(String path, Stream stream, CancellationToken cancelToken)
2016-07-07 13:44:19 +02:00
{
// start a session
var ussa = new UploadSessionStartArg();
var chunksize = (int)Math.Min(DROPBOX_MAX_CHUNK_UPLOAD, stream.Length);
2018-10-23 16:21:36 +02:00
var req = CreateRequest(WebApi.Dropbox.UploadSessionStartUrl(), "POST");
2016-07-07 13:44:19 +02:00
req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(ussa);
req.ContentType = "application/octet-stream";
req.ContentLength = chunksize;
2016-07-07 13:44:19 +02:00
req.Timeout = 200000;
var areq = new AsyncHttpRequest(req);
byte[] buffer = new byte[Utility.Utility.DEFAULT_BUFFER_SIZE];
int sizeToRead = Math.Min((int)Utility.Utility.DEFAULT_BUFFER_SIZE, chunksize);
2016-07-07 13:44:19 +02:00
ulong globalBytesRead = 0;
using (var rs = areq.GetRequestStream())
2025-03-21 10:49:36 +01:00
using (var ts = rs.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout))
2016-07-07 13:44:19 +02:00
{
int bytesRead = 0;
do
{
bytesRead = await stream.ReadAsync(buffer, 0, sizeToRead, cancelToken).ConfigureAwait(false);
globalBytesRead += (ulong)bytesRead;
2025-03-21 10:49:36 +01:00
await ts.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
}
while (bytesRead > 0 && globalBytesRead < (ulong)chunksize);
2016-07-07 13:44:19 +02:00
}
var ussr = await ReadJSONResponseAsync<UploadSessionStartResult>(areq, cancelToken); // pun intended
2016-07-07 13:44:19 +02:00
// keep appending until finished
// 1) read into buffer
while (globalBytesRead < (ulong)stream.Length)
{
var remaining = (ulong)stream.Length - globalBytesRead;
// start an append request
var usaa = new UploadSessionAppendArg();
usaa.cursor.session_id = ussr.session_id;
usaa.cursor.offset = globalBytesRead;
usaa.close = remaining < DROPBOX_MAX_CHUNK_UPLOAD;
chunksize = (int)Math.Min(DROPBOX_MAX_CHUNK_UPLOAD, (long)remaining);
2018-10-23 16:21:36 +02:00
req = CreateRequest(WebApi.Dropbox.UploadSessionAppendUrl(), "POST");
2016-07-07 13:44:19 +02:00
req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(usaa);
req.ContentType = "application/octet-stream";
req.ContentLength = chunksize;
2016-07-07 13:44:19 +02:00
req.Timeout = 200000;
areq = new AsyncHttpRequest(req);
int bytesReadInRequest = 0;
sizeToRead = Math.Min(chunksize, (int)Utility.Utility.DEFAULT_BUFFER_SIZE);
2016-07-07 13:44:19 +02:00
using (var rs = areq.GetRequestStream())
2025-03-21 10:49:36 +01:00
using (var ts = rs.ObserveWriteTimeout(m_timeouts.ReadWriteTimeout))
2016-07-07 13:44:19 +02:00
{
int bytesRead = 0;
do
{
bytesRead = await stream.ReadAsync(buffer, 0, sizeToRead, cancelToken).ConfigureAwait(false);
bytesReadInRequest += bytesRead;
2016-07-07 13:44:19 +02:00
globalBytesRead += (ulong)bytesRead;
2025-03-21 10:49:36 +01:00
await ts.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
while (bytesRead > 0 && bytesReadInRequest < chunksize);
2016-07-07 13:44:19 +02:00
}
2025-03-21 10:49:36 +01:00
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, async ct =>
{
using (var response = GetResponse(areq))
using (var sr = new StreamReader(response.GetResponseStream()))
await sr.ReadToEndAsync().ConfigureAwait(false);
}).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
// finish session and commit
try
{
var usfa = new UploadSessionFinishArg();
2016-07-07 13:44:19 +02:00
usfa.cursor.session_id = ussr.session_id;
2019-01-21 19:30:52 -08:00
usfa.cursor.offset = globalBytesRead;
2016-07-07 13:44:19 +02:00
usfa.commit.path = path;
2018-10-23 16:21:36 +02:00
req = CreateRequest(WebApi.Dropbox.UploadSessionFinishUrl(), "POST");
2016-07-07 13:44:19 +02:00
req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(usfa);
req.ContentType = "application/octet-stream";
req.Timeout = 200000;
2025-03-21 10:49:36 +01:00
return await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, ct =>
ReadJSONResponseAsync<FileMetaData>(req, ct)
).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, true);
2016-07-07 13:44:19 +02:00
throw;
}
}
public async Task DownloadFileAsync(string path, Stream fs, CancellationToken cancelToken)
2016-07-07 13:44:19 +02:00
{
try
{
2018-10-23 16:21:36 +02:00
var pa = new PathArg { path = path };
var req = CreateRequest(WebApi.Dropbox.DownloadFilesUrl(), "POST");
2016-07-07 13:44:19 +02:00
req.Headers[API_ARG_HEADER] = JsonConvert.SerializeObject(pa);
2025-03-21 10:49:36 +01:00
using (var response = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, ct => GetResponse(req)))
using (var rs = await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, ct => response.GetResponseStream()))
using (var ts = rs.ObserveReadTimeout(m_timeouts.ReadWriteTimeout))
await Utility.Utility.CopyStreamAsync(ts, fs, cancelToken).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, true);
2016-07-07 13:44:19 +02:00
throw;
}
}
2024-09-29 23:04:54 +02:00
public async Task DeleteAsync(string path, CancellationToken cancelToken)
2016-07-07 13:44:19 +02:00
{
try
{
2025-03-21 10:49:36 +01:00
await Utility.Utility.WithTimeout(m_timeouts.ShortTimeout, cancelToken, async ct =>
{
var pa = new PathArg() { path = path };
using (var response = await GetResponseAsync(WebApi.Dropbox.DeleteUrl(), ct, pa).ConfigureAwait(false))
using (var sr = new StreamReader(response.GetResponseStream()))
await sr.ReadToEndAsync(ct).ConfigureAwait(false);
}).ConfigureAwait(false);
2016-07-07 13:44:19 +02:00
}
catch (Exception ex)
{
2018-06-23 19:45:11 +02:00
HandleDropboxException(ex, true);
2016-07-07 13:44:19 +02:00
throw;
}
}
2018-06-23 19:45:11 +02:00
private void HandleDropboxException(Exception ex, bool filerequest)
2016-07-07 13:44:19 +02:00
{
2019-09-29 20:16:28 -07:00
if (ex is WebException exception)
{
string json = string.Empty;
try
{
2025-03-21 10:49:36 +01:00
if (exception.Response != null)
using (var sr = new StreamReader(exception.Response.GetResponseStream()))
json = sr.ReadToEnd();
}
catch { }
// Special mapping for exceptions:
// https://www.dropbox.com/developers-v1/core/docs
2019-09-29 20:16:28 -07:00
if (exception.Response is HttpWebResponse httpResp)
{
if (httpResp.StatusCode == HttpStatusCode.NotFound)
{
if (filerequest)
2025-03-21 10:49:36 +01:00
throw new Interface.FileMissingException(json);
else
2025-03-21 10:49:36 +01:00
throw new Interface.FolderMissingException(json);
}
if (httpResp.StatusCode == HttpStatusCode.Conflict)
{
//TODO: Should actually parse and see if something else happens
if (filerequest)
2025-03-21 10:49:36 +01:00
throw new Interface.FileMissingException(json);
else
2025-03-21 10:49:36 +01:00
throw new Interface.FolderMissingException(json);
}
if (httpResp.StatusCode == HttpStatusCode.Unauthorized)
2019-09-29 20:16:28 -07:00
ThrowAuthException(json, exception);
2024-12-20 16:10:08 +01:00
if ((int)httpResp.StatusCode == 429 || (int)httpResp.StatusCode == 507)
2025-03-21 10:49:36 +01:00
throw new Interface.UserInformationException(Strings.Dropbox.OverQuotaError(string.IsNullOrWhiteSpace(json) ? exception.Message : json), "DropboxOverQuotaError", ex);
}
2025-03-21 10:49:36 +01:00
JObject? errJson = null;
try
{
errJson = JObject.Parse(json);
}
catch
{
}
if (errJson != null)
throw new DropboxException() { errorJSON = errJson };
2025-03-20 10:11:30 +01:00
else if (exception.Response is HttpWebResponse wresp)
throw new InvalidDataException($"Non-json response (code: {wresp.StatusCode}, message: {wresp.StatusDescription}): {json}", ex);
else
2025-03-20 10:11:30 +01:00
throw new InvalidDataException($"Non-json response: {json}", ex);
}
2016-07-07 13:44:19 +02:00
}
}
public class DropboxException : Exception
{
2025-03-21 10:49:36 +01:00
public JObject? errorJSON { get; set; }
2016-07-07 13:44:19 +02:00
}
public class PathArg
{
2025-03-21 10:49:36 +01:00
public string? path { get; set; }
2016-07-07 13:44:19 +02:00
}
public class FolderMetadata : MetaData
{
2016-07-07 13:44:19 +02:00
}
public class UploadSessionStartArg
{
2019-04-28 17:02:46 -07:00
// ReSharper disable once UnusedMember.Global
// This is serialized into JSON and provided in the Dropbox request header.
// A value of false indicates that the session should not be closed.
2019-05-27 09:57:18 -07:00
public static bool close => false;
2016-07-07 13:44:19 +02:00
}
public class UploadSessionAppendArg
{
public UploadSessionAppendArg()
{
cursor = new UploadSessionCursor();
}
public UploadSessionCursor cursor { get; set; }
public bool close { get; set; }
}
public class UploadSessionFinishArg
{
public UploadSessionFinishArg()
{
cursor = new UploadSessionCursor();
commit = new CommitInfo();
}
public UploadSessionCursor cursor { get; set; }
public CommitInfo commit { get; set; }
}
public class UploadSessionCursor
{
2025-03-21 10:49:36 +01:00
public string? session_id { get; set; }
2016-07-07 13:44:19 +02:00
public ulong offset { get; set; }
}
public class CommitInfo
{
public CommitInfo()
{
mode = "overwrite";
autorename = false;
mute = true;
}
2025-03-21 10:49:36 +01:00
public string? path { get; set; }
2016-07-07 13:44:19 +02:00
public string mode { get; set; }
public bool autorename { get; set; }
public bool mute { get; set; }
}
public class UploadSessionStartResult
{
2025-03-21 10:49:36 +01:00
public string? session_id { get; set; }
2016-07-07 13:44:19 +02:00
}
public class ListFolderResult
{
2025-03-21 10:49:36 +01:00
public MetaData[]? entries { get; set; }
2016-07-07 13:44:19 +02:00
2025-03-21 10:49:36 +01:00
public string? cursor { get; set; }
2016-07-07 13:44:19 +02:00
public bool has_more { get; set; }
}
public class ListFolderContinueArg
{
2025-03-21 10:49:36 +01:00
public string? cursor { get; set; }
2016-07-07 13:44:19 +02:00
}
public class MetaData
{
[JsonProperty(".tag")]
2025-03-21 10:49:36 +01:00
public string? tag { get; set; }
public string? name { get; set; }
public string? server_modified { get; set; }
2016-07-07 13:44:19 +02:00
public ulong size { get; set; }
public bool IsFile { get { return tag == "file"; } }
2019-05-26 19:13:22 -07:00
// While this is unused, the Dropbox API v2 documentation does not
// declare this to be optional.
// ReSharper disable once UnusedMember.Global
2025-03-21 10:49:36 +01:00
public string? id { get; set; }
2019-05-26 19:13:22 -07:00
// While this is unused, the Dropbox API v2 documentation does not
// declare this to be optional.
// ReSharper disable once UnusedMember.Global
2025-03-21 10:49:36 +01:00
public string? rev { get; set; }
2016-07-07 13:44:19 +02:00
}
public class FileMetaData : MetaData
{
2016-07-07 13:44:19 +02:00
}
2016-07-07 13:44:19 +02:00
}