Fixed timeout logic to guard all Http calls
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Library.Utility.Options;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
@@ -137,6 +138,11 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// </summary>
|
||||
private string? _validationKey;
|
||||
|
||||
/// <summary>
|
||||
/// The timeout configuration
|
||||
/// </summary>
|
||||
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a relogin has been attempted
|
||||
/// </summary>
|
||||
@@ -160,11 +166,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// <param name="email">The email address for authentication</param>
|
||||
/// <param name="password">The password for authentication</param>
|
||||
/// <param name="device_id">The device ID for authentication</param>
|
||||
private MovistarCloudApiClient(string email, string password, string device_id)
|
||||
/// <param name="timeouts">The timeout configuration</param>
|
||||
private MovistarCloudApiClient(string email, string password, string device_id, TimeoutOptionsHelper.Timeouts timeouts)
|
||||
{
|
||||
_email = email ?? throw new ArgumentNullException(nameof(email));
|
||||
_password = password ?? throw new ArgumentNullException(nameof(password));
|
||||
_device_id = device_id;
|
||||
_timeouts = timeouts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -173,11 +181,12 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// <param name="email">The email address for authentication</param>
|
||||
/// <param name="password">The password for authentication</param>
|
||||
/// <param name="device_id">The device ID for authentication</param>
|
||||
/// <param name="timeouts">The timeout configuration</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The initialized API client</returns>
|
||||
public static async Task<MovistarCloudApiClient> CreateAsync(string email, string password, string device_id, CancellationToken cancellationToken = default)
|
||||
public static async Task<MovistarCloudApiClient> CreateAsync(string email, string password, string device_id, TimeoutOptionsHelper.Timeouts timeouts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var client = new MovistarCloudApiClient(email, password, device_id);
|
||||
var client = new MovistarCloudApiClient(email, password, device_id, timeouts);
|
||||
await client.EnsureLoggedInAsync(cancellationToken).ConfigureAwait(false);
|
||||
return client;
|
||||
}
|
||||
@@ -250,6 +259,7 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
};
|
||||
|
||||
_http = HttpClientHelper.CreateClient(handler);
|
||||
_http.Timeout = Timeout.InfiniteTimeSpan;
|
||||
_http.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
|
||||
}
|
||||
|
||||
@@ -273,15 +283,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// Reads the response body as JSON or throws if it looks like HTML.
|
||||
/// </summary>
|
||||
/// <param name="resp">The HTTP response</param>
|
||||
/// <param name="timeout">The read timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The response body as a string</returns>
|
||||
/// <exception cref="MovistarCloudSessionExpiredException">Thrown when the session appears expired</exception>
|
||||
private async Task<string> ReadJsonOrThrowAsync(HttpResponseMessage resp, TimeSpan timeout, CancellationToken ct)
|
||||
private async Task<string> ReadJsonOrThrowAsync(HttpResponseMessage resp, CancellationToken ct)
|
||||
{
|
||||
await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
|
||||
using var timeoutStream = stream.ObserveReadTimeout(timeout, false);
|
||||
using var reader = new StreamReader(timeoutStream, Encoding.UTF8);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
var body = await reader.ReadToEndAsync(ct).ConfigureAwait(false);
|
||||
|
||||
if (LooksLikeHtml(resp, body))
|
||||
@@ -315,10 +323,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
};
|
||||
|
||||
using var http = HttpClientHelper.CreateClient(handler);
|
||||
http.Timeout = Timeout.InfiniteTimeSpan;
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
|
||||
|
||||
// Any public/light endpoint works; we use the home page
|
||||
using var respInitial = await http.GetAsync($"{BaseUrl}/", ct).ConfigureAwait(false);
|
||||
using var respInitial = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, cts =>
|
||||
http.GetAsync($"{BaseUrl}/", cts)
|
||||
).ConfigureAwait(false);
|
||||
respInitial.EnsureSuccessStatusCode();
|
||||
|
||||
var url = $"{BaseUrl}{LoginPath}";
|
||||
@@ -332,9 +343,12 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
req.Content = content;
|
||||
req.Headers.Add("X-Deviceid", _device_id);
|
||||
|
||||
using var resp = await http.SendAsync(req, ct);
|
||||
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await http.SendAsync(req, cts).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return await resp.Content.ReadAsStringAsync(cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var data = doc.RootElement.GetProperty("data");
|
||||
@@ -353,14 +367,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="folderId">The folder ID (0 for root)</param>
|
||||
/// <param name="limit">The maximum number of items to return</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The list of remote files</returns>
|
||||
public async Task<List<RemoteFile>> ListFilesAsync(long folderId, int limit, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<List<RemoteFile>> ListFilesAsync(long folderId, int limit, CancellationToken ct)
|
||||
{
|
||||
if (folderId == 0)
|
||||
{
|
||||
folderId = await GetRootFolderIdAsync(timeout, ct).ConfigureAwait(false);
|
||||
folderId = await GetRootFolderIdAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var result = new List<RemoteFile>();
|
||||
@@ -368,10 +381,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
// List Files
|
||||
var url = AddValidationKey($"{BaseUrl}{MediaListPath}&folderid={folderId}&limit={limit}");
|
||||
var payload = new { data = new { fields = new[] { "name", "modificationdate", "size", "etag" } } };
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var media = doc.RootElement.GetProperty("data").GetProperty("media");
|
||||
@@ -391,10 +407,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
// List Directories
|
||||
url = AddValidationKey($"{BaseUrl}{FolderListPath}&parentid={folderId}&limit={limit}");
|
||||
payload = new { data = new { fields = new[] { "name", "date" } } };
|
||||
|
||||
using var contentDir = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var respDir = await Http.PostAsync(url, contentDir, ct).ConfigureAwait(false);
|
||||
body = await ReadJsonOrThrowAsync(respDir, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
body = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, ct, async cts =>
|
||||
{
|
||||
using var respDir = await Http.PostAsync(url, contentDir, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(respDir, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var docDir = JsonDocument.Parse(body);
|
||||
media = docDir.RootElement.GetProperty("data").GetProperty("folders");
|
||||
@@ -420,10 +439,9 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// <param name="folderId">The destination folder ID</param>
|
||||
/// <param name="remoteName">The remote file name</param>
|
||||
/// <param name="localFilePath">The local file path</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The upload result</returns>
|
||||
public async Task<UploadResult> UploadFileAsync(long folderId, string remoteName, string localFilePath, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<UploadResult> UploadFileAsync(long folderId, string remoteName, string localFilePath, CancellationToken ct)
|
||||
{
|
||||
var fi = new FileInfo(localFilePath);
|
||||
if (!fi.Exists) throw new FileNotFoundException("Local file not found", localFilePath);
|
||||
@@ -446,13 +464,15 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
form.Add(new StringContent(JsonSerializer.Serialize(meta), Encoding.UTF8, "application/json"), "data");
|
||||
|
||||
await using var fs = File.OpenRead(localFilePath);
|
||||
await using var timeoutStream = fs.ObserveReadTimeout(timeout, false);
|
||||
await using var timeoutStream = fs.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
|
||||
var filePart = new StreamContent(timeoutStream);
|
||||
filePart.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
form.Add(filePart, "file", remoteName);
|
||||
|
||||
using var resp = await Http.PostAsync(url, form, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, cts =>
|
||||
ReadJsonOrThrowAsync(resp, cts)
|
||||
).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var idElem = doc.RootElement.GetProperty("id");
|
||||
@@ -467,17 +487,19 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// Gets the validation status of an uploaded file.
|
||||
/// </summary>
|
||||
/// <param name="id">The file ID</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The validation status string</returns>
|
||||
public async Task<string> GetValidationStatusAsync(long id, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<string> GetValidationStatusAsync(long id, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{ValidationStatusPath}");
|
||||
var payload = new { data = new { ids = new[] { new { id } } } };
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return doc.RootElement.GetProperty("data").GetProperty("ids")[0].GetProperty("status").GetString() ?? "";
|
||||
@@ -487,17 +509,19 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// Gets a signed download URL for a file.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The signed download URL</returns>
|
||||
public async Task<string> GetDownloadUrlAsync(long fileId, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<string> GetDownloadUrlAsync(long fileId, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{DownloadUrlPath}");
|
||||
var payload = new { data = new { ids = new[] { fileId }, fields = new[] { "url", "name", "size", "etag" } } };
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return doc.RootElement.GetProperty("data").GetProperty("media")[0].GetProperty("url").GetString()!;
|
||||
@@ -508,48 +532,56 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="signedUrl">The signed URL to download from</param>
|
||||
/// <param name="localFilePath">The local file path to save to</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
public async Task DownloadToFileAsync(string signedUrl, string localFilePath, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task DownloadToFileAsync(string signedUrl, string localFilePath, CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(localFilePath) ?? ".");
|
||||
using var resp = await Http.GetAsync(signedUrl, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
using var resp = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, cts =>
|
||||
Http.GetAsync(signedUrl, HttpCompletionOption.ResponseHeadersRead, cts)
|
||||
).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
await using var rs = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
|
||||
await using var timeoutReadStream = rs.ObserveReadTimeout(timeout, false);
|
||||
await using var rs = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, cts =>
|
||||
resp.Content.ReadAsStreamAsync(cts)
|
||||
).ConfigureAwait(false);
|
||||
|
||||
await using var fs = File.Create(localFilePath);
|
||||
await using var timeoutWriteStream = fs.ObserveWriteTimeout(timeout, false);
|
||||
await timeoutReadStream.CopyToAsync(timeoutWriteStream, ct).ConfigureAwait(false);
|
||||
await using var timeoutWriteStream = fs.ObserveWriteTimeout(_timeouts.ReadWriteTimeout, false);
|
||||
await rs.CopyToAsync(timeoutWriteStream, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Soft deletes a file (moves to trash).
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID to delete</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
public async Task SoftDeleteFileAsync(long fileId, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task SoftDeleteFileAsync(long fileId, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{DeletePath}");
|
||||
var payload = new { data = new { files = new[] { fileId } } };
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
_ = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
_ = await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the storage space information.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The storage space information</returns>
|
||||
public async Task<StorageSpace> GetStorageSpaceAsync(TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<StorageSpace> GetStorageSpaceAsync(CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{StorageSpacePath}");
|
||||
using var resp = await Http.PostAsync(url, new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"), ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"), cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var d = doc.RootElement.GetProperty("data");
|
||||
@@ -565,10 +597,9 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// Lists items in the trash.
|
||||
/// </summary>
|
||||
/// <param name="pageSize">The number of items to return</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The list of trash entries</returns>
|
||||
public async Task<List<TrashEntry>> ListTrashAsync(int pageSize, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<List<TrashEntry>> ListTrashAsync(int pageSize, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{TrashPath}");
|
||||
|
||||
@@ -580,10 +611,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
["origin"] = new[] { "omh", "dropbox" }
|
||||
}
|
||||
};
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var entries = doc.RootElement.GetProperty("data").GetProperty("entries");
|
||||
@@ -606,15 +640,17 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent folder ID</param>
|
||||
/// <param name="name">The folder name to search for</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The folder ID if found, null otherwise</returns>
|
||||
public async Task<long?> TryGetFolderIdByNameAsync(long parentId, string name, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<long?> TryGetFolderIdByNameAsync(long parentId, string name, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{FolderListPath}&parentid={parentId}&limit=2000");
|
||||
|
||||
using var resp = await Http.GetAsync(url, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.GetAsync(url, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var folders = doc.RootElement.GetProperty("data").GetProperty("folders");
|
||||
@@ -635,24 +671,22 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent folder ID</param>
|
||||
/// <param name="name">The folder name to create</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The created or existing folder ID</returns>
|
||||
public async Task<long> CreateFolderAsync(long parentId, string name, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<long> CreateFolderAsync(long parentId, string name, CancellationToken ct)
|
||||
{
|
||||
if (parentId == 0)
|
||||
{
|
||||
parentId = await GetRootFolderIdAsync(timeout, ct).ConfigureAwait(false);
|
||||
parentId = await GetRootFolderIdAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Idempotency: if it already exists, return its id
|
||||
var existing = await TryGetFolderIdByNameAsync(parentId, name, timeout, ct).ConfigureAwait(false);
|
||||
var existing = await TryGetFolderIdByNameAsync(parentId, name, ct).ConfigureAwait(false);
|
||||
if (existing.HasValue)
|
||||
return existing.Value;
|
||||
|
||||
// Create folder
|
||||
var url = AddValidationKey($"{BaseUrl}{FolderCreatePath}");
|
||||
|
||||
var payload = new
|
||||
{
|
||||
data = new
|
||||
@@ -663,10 +697,13 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
parentid = parentId
|
||||
}
|
||||
};
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
using var resp = await Http.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.PostAsync(url, content, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var createdFolder = doc.RootElement.GetProperty("data").GetProperty("folder");
|
||||
@@ -678,14 +715,17 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// <summary>
|
||||
/// Gets the root folder ID for the current user.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The root folder ID</returns>
|
||||
public async Task<long> GetRootFolderIdAsync(TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<long> GetRootFolderIdAsync(CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{RootFolderPath}");
|
||||
using var resp = await Http.GetAsync(url, ct).ConfigureAwait(false);
|
||||
var body = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
var body = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.GetAsync(url, cts).ConfigureAwait(false);
|
||||
return await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var folders = doc.RootElement.GetProperty("data").GetProperty("folders");
|
||||
@@ -696,41 +736,43 @@ internal sealed class MovistarCloudApiClient : IDisposable
|
||||
/// Asserts that a folder exists by checking if it can be listed.
|
||||
/// </summary>
|
||||
/// <param name="folderId">The folder ID to check</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
public async Task AssertFolderExistsByIdAsync(long folderId, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task AssertFolderExistsByIdAsync(long folderId, CancellationToken ct)
|
||||
{
|
||||
var url = AddValidationKey($"{BaseUrl}{MediaListPath}&folderid={folderId}&limit=1");
|
||||
using var resp = await Http.GetAsync(url, ct).ConfigureAwait(false);
|
||||
_ = await ReadJsonOrThrowAsync(resp, timeout, ct).ConfigureAwait(false);
|
||||
|
||||
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, ct, async cts =>
|
||||
{
|
||||
using var resp = await Http.GetAsync(url, cts).ConfigureAwait(false);
|
||||
_ = await ReadJsonOrThrowAsync(resp, cts).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a folder path exists, creating parent folders as needed.
|
||||
/// </summary>
|
||||
/// <param name="path">The folder path to ensure</param>
|
||||
/// <param name="timeout">The operation timeout</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The folder ID of the final path segment</returns>
|
||||
public async Task<long> EnsureFolderPathAsync(string path, TimeSpan timeout, CancellationToken ct)
|
||||
public async Task<long> EnsureFolderPathAsync(string path, CancellationToken ct)
|
||||
{
|
||||
var p = path.Trim();
|
||||
if (string.IsNullOrWhiteSpace(p))
|
||||
throw new ArgumentException("root-folder-path empty");
|
||||
|
||||
var segments = p.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var currentId = await GetRootFolderIdAsync(timeout, ct).ConfigureAwait(false);
|
||||
var currentId = await GetRootFolderIdAsync(ct).ConfigureAwait(false);
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var existing = await TryGetFolderIdByNameAsync(currentId, segment, timeout, ct).ConfigureAwait(false);
|
||||
var existing = await TryGetFolderIdByNameAsync(currentId, segment, ct).ConfigureAwait(false);
|
||||
if (existing.HasValue)
|
||||
{
|
||||
currentId = existing.Value;
|
||||
continue;
|
||||
}
|
||||
|
||||
currentId = await CreateFolderAsync(currentId, segment, timeout, ct).ConfigureAwait(false);
|
||||
currentId = await CreateFolderAsync(currentId, segment, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return currentId;
|
||||
|
||||
@@ -47,15 +47,15 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// <summary>
|
||||
/// The device ID option name coming from the X-Deviceid header
|
||||
/// </summary>
|
||||
private const string DeviceIdOption = "deviceID";
|
||||
private const string DeviceIdOption = "deviceID";
|
||||
/// <summary>
|
||||
/// The list limit option name
|
||||
/// </summary>
|
||||
private const string ListLimitOption = "list-limit";
|
||||
/// <summary>
|
||||
/// The wait for validation option name
|
||||
/// The ignore validation result option name
|
||||
/// </summary>
|
||||
private const string WaitForValidationOption = "wait-for-validation";
|
||||
private const string IgnoreValidationResultOption = "ignore-validation-result";
|
||||
/// <summary>
|
||||
/// The validation timeout option name
|
||||
/// </summary>
|
||||
@@ -82,10 +82,6 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// </summary>
|
||||
private const int DefaultListLimit = 2000;
|
||||
/// <summary>
|
||||
/// The default wait for validation value
|
||||
/// </summary>
|
||||
private const bool DefaultWaitForValidation = true;
|
||||
/// <summary>
|
||||
/// The default validation timeout value
|
||||
/// </summary>
|
||||
private const string DefaultValidationTimeout = "10m";
|
||||
@@ -125,47 +121,47 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// The root folder ID
|
||||
/// </summary>
|
||||
private long _rootFolderId;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return per listing call
|
||||
/// </summary>
|
||||
private readonly int _listLimit;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether to wait until uploaded file becomes usable (status=U)
|
||||
/// </summary>
|
||||
private readonly bool _waitForValidation;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time to wait for server-side upload validation
|
||||
/// </summary>
|
||||
private readonly TimeSpan _validationTimeout;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The polling interval for validation status
|
||||
/// </summary>
|
||||
private readonly TimeSpan _validationPollInterval;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagnostics logging is enabled
|
||||
/// </summary>
|
||||
private readonly bool _diagnostics;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The diagnostics level (basic or trash)
|
||||
/// </summary>
|
||||
private readonly DiagnosticsLevel _diagnosticsLevel;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The number of items to list from trash when diagnostics-level=trash
|
||||
/// </summary>
|
||||
private readonly int _trashPageSize;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The optional root folder path
|
||||
/// </summary>
|
||||
private readonly string? _rootFolderPathOpt;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the destination has been resolved
|
||||
/// </summary>
|
||||
@@ -174,32 +170,56 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// <summary>
|
||||
/// The API client instance
|
||||
/// </summary>
|
||||
private readonly MovistarCloudApiClient? _client;
|
||||
|
||||
private MovistarCloudApiClient? _client;
|
||||
|
||||
/// <summary>
|
||||
/// The email address for authentication
|
||||
/// </summary>
|
||||
private readonly string _email;
|
||||
|
||||
/// <summary>
|
||||
/// The password for authentication
|
||||
/// </summary>
|
||||
private readonly string _password;
|
||||
|
||||
/// <summary>
|
||||
/// The device ID for authentication
|
||||
/// </summary>
|
||||
private readonly string _deviceId;
|
||||
|
||||
/// <summary>
|
||||
/// The mapping of file names to IDs
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, long> _nameToId = new(StringComparer.Ordinal);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The timeout configuration
|
||||
/// </summary>
|
||||
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the API client, throwing if not initialized
|
||||
/// Gets or creates the API client, initializing it on first use.
|
||||
/// </summary>
|
||||
private MovistarCloudApiClient Client
|
||||
=> _client ?? throw new InvalidOperationException(
|
||||
"Backend not initialized. This instance was created using the default constructor for metadata only."
|
||||
);
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns>The API client</returns>
|
||||
private async Task<MovistarCloudApiClient> GetClientAsync(CancellationToken ct)
|
||||
{
|
||||
if (_client != null)
|
||||
return _client;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_email) || string.IsNullOrWhiteSpace(_password) || string.IsNullOrWhiteSpace(_deviceId))
|
||||
throw new InvalidOperationException("Backend not initialized with credentials.");
|
||||
|
||||
_client = await MovistarCloudApiClient.CreateAsync(_email, _password, _deviceId, _timeouts, ct).ConfigureAwait(false);
|
||||
return _client;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName => Strings.MovistarCloudBackend.DisplayName;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ProtocolKey => "movistarcloud";
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => Strings.MovistarCloudBackend.Description;
|
||||
|
||||
@@ -208,9 +228,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
[
|
||||
new CommandLineArgument(EmailOption, CommandLineArgument.ArgumentType.String, Strings.MovistarCloudBackend.EmailShort, Strings.MovistarCloudBackend.EmailLong),
|
||||
new CommandLineArgument(PasswordOption, CommandLineArgument.ArgumentType.Password, Strings.MovistarCloudBackend.PasswordShort, Strings.MovistarCloudBackend.PasswordLong),
|
||||
new CommandLineArgument(DeviceIdOption, CommandLineArgument.ArgumentType.String, Strings.MovistarCloudBackend.DeviceIdShort, Strings.MovistarCloudBackend.DeviceIdLong),
|
||||
new CommandLineArgument(DeviceIdOption, CommandLineArgument.ArgumentType.String, Strings.MovistarCloudBackend.DeviceIdShort, Strings.MovistarCloudBackend.DeviceIdLong),
|
||||
new CommandLineArgument(ListLimitOption, CommandLineArgument.ArgumentType.Integer, Strings.MovistarCloudBackend.ListLimitShort, Strings.MovistarCloudBackend.ListLimitLong, DefaultListLimit.ToString()),
|
||||
new CommandLineArgument(WaitForValidationOption, CommandLineArgument.ArgumentType.Boolean, Strings.MovistarCloudBackend.WaitForValidationShort, Strings.MovistarCloudBackend.WaitForValidationLong, DefaultWaitForValidation.ToString()),
|
||||
new CommandLineArgument(IgnoreValidationResultOption, CommandLineArgument.ArgumentType.Boolean, Strings.MovistarCloudBackend.IgnoreValidationResultShort, Strings.MovistarCloudBackend.IgnoreValidationResultLong, false.ToString()),
|
||||
new CommandLineArgument(ValidationTimeoutOption, CommandLineArgument.ArgumentType.Timespan, Strings.MovistarCloudBackend.ValidationTimeoutShort, Strings.MovistarCloudBackend.ValidationTimeoutLong, DefaultValidationTimeout),
|
||||
new CommandLineArgument(ValidationPollIntervalOption, CommandLineArgument.ArgumentType.Timespan, Strings.MovistarCloudBackend.ValidationPollIntervalShort, Strings.MovistarCloudBackend.ValidationPollIntervalLong, DefaultValidationPollInterval),
|
||||
new CommandLineArgument(DiagnosticsOption, CommandLineArgument.ArgumentType.Boolean, Strings.MovistarCloudBackend.DiagnosticsShort, Strings.MovistarCloudBackend.DiagnosticsLong, DefaultDiagnostics.ToString()),
|
||||
@@ -225,7 +245,7 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
public MovistarCloudBackend()
|
||||
{
|
||||
_listLimit = DefaultListLimit;
|
||||
_waitForValidation = DefaultWaitForValidation;
|
||||
_waitForValidation = true;
|
||||
_validationTimeout = Timeparser.ParseTimeSpan(DefaultValidationTimeout);
|
||||
_validationPollInterval = Timeparser.ParseTimeSpan(DefaultValidationPollInterval);
|
||||
_diagnostics = DefaultDiagnostics;
|
||||
@@ -233,6 +253,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
_trashPageSize = DefaultTrashPageSize;
|
||||
_timeouts = null!;
|
||||
_rootFolderPathOpt = null;
|
||||
_email = null!;
|
||||
_password = null!;
|
||||
_deviceId = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -242,20 +265,15 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// <param name="options">The options dictionary</param>
|
||||
public MovistarCloudBackend(string url, Dictionary<string, string?> options)
|
||||
{
|
||||
var email = RequireOption(options, EmailOption);
|
||||
var password = RequireOption(options, PasswordOption);
|
||||
var device_id = RequireOption(options, DeviceIdOption);
|
||||
|
||||
_email = RequireOption(options, EmailOption);
|
||||
_password = RequireOption(options, PasswordOption);
|
||||
_deviceId = RequireOption(options, DeviceIdOption);
|
||||
|
||||
var uri = new Utility.Uri(url);
|
||||
_rootFolderPathOpt = uri.HostAndPath;
|
||||
|
||||
_listLimit = Library.Utility.Utility.ParseIntOption(options, ListLimitOption, DefaultListLimit);
|
||||
|
||||
//TODO: Possible replacement of ParseBoolOption to accept default value.
|
||||
if (options.ContainsKey(WaitForValidationOption))
|
||||
_waitForValidation = Library.Utility.Utility.ParseBoolOption(options, WaitForValidationOption);
|
||||
else _waitForValidation = DefaultWaitForValidation;
|
||||
_listLimit = Library.Utility.Utility.ParseIntOption(options, ListLimitOption, DefaultListLimit);
|
||||
_waitForValidation = !Library.Utility.Utility.ParseBoolOption(options, IgnoreValidationResultOption);
|
||||
|
||||
_validationTimeout = Library.Utility.Utility.ParseTimespanOption(options, ValidationTimeoutOption, DefaultValidationTimeout);
|
||||
_validationPollInterval = Library.Utility.Utility.ParseTimespanOption(options, ValidationPollIntervalOption, DefaultValidationPollInterval);
|
||||
@@ -264,11 +282,8 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
_trashPageSize = Math.Clamp(Library.Utility.Utility.ParseIntOption(options, TrashPageSizeOption, DefaultTrashPageSize), 1, 200);
|
||||
|
||||
_timeouts = TimeoutOptionsHelper.Parse(options);
|
||||
_client = MovistarCloudApiClient.CreateAsync(email, password, device_id, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the destination folder is resolved, creating it if necessary.
|
||||
/// </summary>
|
||||
@@ -278,21 +293,23 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
if (_destinationResolved)
|
||||
return;
|
||||
|
||||
var client = await GetClientAsync(ct).ConfigureAwait(false);
|
||||
|
||||
if (_rootFolderId > 0)
|
||||
{
|
||||
await Client.WithAutoRelogin(x => Client.AssertFolderExistsByIdAsync(_rootFolderId, _timeouts.ShortTimeout, x), ct);
|
||||
await client.WithAutoRelogin(x => client.AssertFolderExistsByIdAsync(_rootFolderId, x), ct);
|
||||
_destinationResolved = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_rootFolderPathOpt))
|
||||
{
|
||||
var id = await Client.WithAutoRelogin(x => Client.EnsureFolderPathAsync(_rootFolderPathOpt!, _timeouts.ShortTimeout, x), ct);
|
||||
var id = await client.WithAutoRelogin(x => client.EnsureFolderPathAsync(_rootFolderPathOpt!, x), ct);
|
||||
_rootFolderId = id;
|
||||
_destinationResolved = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
throw new FolderMissingException("Missing destination folder: specify path on configuration.");
|
||||
}
|
||||
|
||||
@@ -302,8 +319,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
await EnsureDestinationResolvedAsync(cancellationToken).ConfigureAwait(false);
|
||||
ValidateRemoteName(remotename);
|
||||
|
||||
var upload = await Client.WithAutoRelogin(
|
||||
ct => Client.UploadFileAsync(_rootFolderId, remotename, filename, _timeouts.ReadWriteTimeout, ct),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
var upload = await client.WithAutoRelogin(
|
||||
ct => client.UploadFileAsync(_rootFolderId, remotename, filename, ct),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (_waitForValidation)
|
||||
@@ -312,8 +330,8 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
var usableStatus = false;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
var st = await Client.WithAutoRelogin(
|
||||
ct => Client.GetValidationStatusAsync(upload.Id, _timeouts.ShortTimeout, ct),
|
||||
var st = await client.WithAutoRelogin(
|
||||
ct => client.GetValidationStatusAsync(upload.Id, ct),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.Equals(st, "U", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -328,7 +346,7 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
{
|
||||
//Right now we dont see other cases appart of status "V" and Status "U"
|
||||
//TODO: validate new status in the previous block if they appear
|
||||
Logging.Log.WriteWarningMessage(LOGTAG, "ValidationTimeout",null, "Validation did not reach status=U within {0} for {1} (uploadId={2}). Continuing as best-effort.",_validationTimeout, remotename, upload.Id);
|
||||
Logging.Log.WriteWarningMessage(LOGTAG, "ValidationTimeout", null, "Validation did not reach status=U within {0} for {1} (uploadId={2}). Continuing as best-effort.", _validationTimeout, remotename, upload.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,12 +358,15 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
{
|
||||
await EnsureDestinationResolvedAsync(cancellationToken).ConfigureAwait(false);
|
||||
var id = await ResolveIdByNameAsync(remotename, cancellationToken).ConfigureAwait(false);
|
||||
if (id == null)
|
||||
throw new FileMissingException($"File {remotename} not found in Movistar Cloud.");
|
||||
|
||||
var signedUrl = await Client.WithAutoRelogin(
|
||||
ct => Client.GetDownloadUrlAsync((long)id, _timeouts.ShortTimeout, ct),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
var signedUrl = await client.WithAutoRelogin(
|
||||
ct => client.GetDownloadUrlAsync((long)id, ct),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await Client.DownloadToFileAsync(signedUrl, filename, _timeouts.ReadWriteTimeout, cancellationToken).ConfigureAwait(false);
|
||||
await client.DownloadToFileAsync(signedUrl, filename, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -355,8 +376,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
var id = await ResolveIdByNameAsync(remotename, cancellationToken, allowMissing: true).ConfigureAwait(false);
|
||||
if (id == null) return;
|
||||
|
||||
await Client.WithAutoRelogin(
|
||||
ct => Client.SoftDeleteFileAsync(id.Value, _timeouts.ShortTimeout, ct),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
await client.WithAutoRelogin(
|
||||
ct => client.SoftDeleteFileAsync(id.Value, ct),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_nameToId.Remove(remotename);
|
||||
@@ -365,19 +387,21 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
/// <inheritdoc/>
|
||||
public async Task TestAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Case A: we already have an id -> check that it exists
|
||||
if (_rootFolderId > 0)
|
||||
{
|
||||
await Client.WithAutoRelogin(
|
||||
ct => Client.AssertFolderExistsByIdAsync(_rootFolderId, _timeouts.ShortTimeout, ct),
|
||||
await client.WithAutoRelogin(
|
||||
ct => client.AssertFolderExistsByIdAsync(_rootFolderId, ct),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
// Case B: no id but there is a path -> resolve/create and fill _rootFolderId
|
||||
else if (!string.IsNullOrWhiteSpace(_rootFolderPathOpt))
|
||||
{
|
||||
var resolvedId = await Client.WithAutoRelogin(
|
||||
ct => Client.EnsureFolderPathAsync(_rootFolderPathOpt!, _timeouts.ShortTimeout, ct),
|
||||
var resolvedId = await client.WithAutoRelogin(
|
||||
ct => client.EnsureFolderPathAsync(_rootFolderPathOpt!, ct),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
@@ -388,7 +412,7 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
}
|
||||
// Case C: nothing -> indicate missing folder
|
||||
else
|
||||
{
|
||||
{
|
||||
throw new FolderMissingException("Missing destination folder during Test: specify path on configuration.");
|
||||
}
|
||||
|
||||
@@ -407,8 +431,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
{
|
||||
try
|
||||
{
|
||||
var space = await Client.WithAutoRelogin(
|
||||
ct => Client.GetStorageSpaceAsync(_timeouts.ListTimeout, ct),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
var space = await client.WithAutoRelogin(
|
||||
ct => client.GetStorageSpaceAsync(ct),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
@@ -418,8 +443,8 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
|
||||
if (_diagnosticsLevel == DiagnosticsLevel.Trash)
|
||||
{
|
||||
var trash = await Client.WithAutoRelogin(
|
||||
ct => Client.ListTrashAsync(_trashPageSize, _timeouts.ListTimeout, ct),
|
||||
var trash = await client.WithAutoRelogin(
|
||||
ct => client.ListTrashAsync(_trashPageSize, ct),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
@@ -443,8 +468,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
{
|
||||
await EnsureDestinationResolvedAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var files = await Client.WithAutoRelogin(
|
||||
ct => Client.ListFilesAsync(_rootFolderId, _listLimit, _timeouts.ListTimeout, ct),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
var files = await client.WithAutoRelogin(
|
||||
ct => client.ListFilesAsync(_rootFolderId, _listLimit, ct),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
@@ -465,14 +491,15 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_rootFolderPathOpt))
|
||||
{
|
||||
var id = await Client.WithAutoRelogin(
|
||||
x => Client.EnsureFolderPathAsync(_rootFolderPathOpt!, _timeouts.ShortTimeout, x),
|
||||
var client = await GetClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
var id = await client.WithAutoRelogin(
|
||||
x => client.EnsureFolderPathAsync(_rootFolderPathOpt!, x),
|
||||
cancellationToken);
|
||||
_rootFolderId = id;
|
||||
_destinationResolved = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
throw new UserInformationException(
|
||||
"Unable to create the destination folder because no destination path was provided. Specify Path so the Movistar Cloud backend knows which remote folder to create or use.",
|
||||
"MovistarCloudMissingRootFolderPath");
|
||||
@@ -495,8 +522,9 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
if (_nameToId.TryGetValue(remotename, out var id))
|
||||
return id;
|
||||
|
||||
var files = await Client.WithAutoRelogin(
|
||||
x => Client.ListFilesAsync(_rootFolderId, _listLimit, _timeouts.ListTimeout, x),
|
||||
var client = await GetClientAsync(ct).ConfigureAwait(false);
|
||||
var files = await client.WithAutoRelogin(
|
||||
x => client.ListFilesAsync(_rootFolderId, _listLimit, x),
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
_nameToId.Clear();
|
||||
@@ -561,22 +589,22 @@ public sealed class MovistarCloudBackend : IBackend
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long Size { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFolder { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastAccess { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastModification { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime Created { get; }
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsArchived { get; }
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace Duplicati.Library.Backend.MovistarCloud.Strings
|
||||
public static string RootFolderPathLong => LC.L(@"The path where to store the backup (e.g., /Duplicati/Backups/Computer).");
|
||||
public static string ListLimitShort => LC.L(@"List limit");
|
||||
public static string ListLimitLong => LC.L(@"The maximum number of items returned per listing call.");
|
||||
public static string WaitForValidationShort => LC.L(@"Wait for validation");
|
||||
public static string WaitForValidationLong => LC.L(@"Wait until the uploaded file becomes usable (status=U).");
|
||||
public static string IgnoreValidationResultShort => LC.L(@"Ignore validation result");
|
||||
public static string IgnoreValidationResultLong => LC.L(@"If set, the backend will not wait for server-side upload validation and will continue immediately after the upload.");
|
||||
public static string ValidationTimeoutShort => LC.L(@"Validation timeout");
|
||||
public static string ValidationTimeoutLong => LC.L(@"The maximum time to wait for server-side upload validation.");
|
||||
public static string ValidationPollIntervalShort => LC.L(@"Validation poll interval");
|
||||
|
||||
Reference in New Issue
Block a user