// 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.Net; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Duplicati.WebserverCore.Dto; using Duplicati.WebserverCore.Dto.V2; using Duplicati.WebserverCore.Endpoints.V1.Backup; namespace Duplicati.WebserverCore.Client; /// /// A client for interacting with the Duplicati server API, supporting both v1 and v2 endpoints. /// public class DuplicatiServerClient : IDisposable { private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; private readonly bool _selfOwnedHttpClient; private readonly SemaphoreSlim _tokenRefreshSemaphore = new(1, 1); private readonly ServerCredentialType _credentialType; private readonly string _credential; /// /// Gets a value indicating whether the client is authenticated. /// public bool IsAuthenticated => !string.IsNullOrEmpty(_httpClient.DefaultRequestHeaders.Authorization?.Parameter); /// /// Initializes a new instance of the class. /// /// The base URL of the Duplicati server. /// The type of credential being provided (Password or Token). /// The server password or access token. /// Optional HttpClient instance. If not provided, a new one will be created. public DuplicatiServerClient(string baseUrl, ServerCredentialType credentialType, string credential, HttpClient? httpClient = null) { _selfOwnedHttpClient = httpClient is not null; _baseUrl = baseUrl.TrimEnd('/'); _httpClient = httpClient ?? new HttpClient(); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, AllowTrailingCommas = true }; _credential = credential; _credentialType = credentialType; } /// /// Authenticates the client with the Duplicati server using the provided credentials and acquires a bearer token. /// /// If a call is made to another method before calling Authenticate, it will automatically call this method to ensure the client is authenticated. /// The idea of having a separate Authenticate method is to allow for explicit authentication to avoid getting a 401 result on the server log. /// /// The cancellation token. Optional, defaults to . public async Task Authenticate(CancellationToken cancellationToken = default) { await RefreshTokenAsync(cancellationToken).ConfigureAwait(false); } /// /// Acquires a bearer token using password-based authentication. /// /// The server password. /// The cancellation token. private async Task AcquireBearerViaPasswordAuthAsync(string password, CancellationToken cancellationToken) { var loginResult = await LoginV1Async(new LoginInputDto(password, true), cancellationToken).ConfigureAwait(false); SetTokenAuthentication(loginResult.AccessToken); } /// /// Acquires a bearer token using token-based authentication. /// /// The signin token. /// The cancellation token. private async Task AcquireBearerViaTokenAuthAsync(string token, CancellationToken cancellationToken) { var signinResult = await SigninV1Async(new SigninInputDto(token, true), cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(signinResult.AccessToken)) SetTokenAuthentication(signinResult.AccessToken); } /// /// Sets token-based authentication. /// /// The authentication token. public void SetTokenAuthentication(string token) { if (string.IsNullOrWhiteSpace(token)) throw new ArgumentException("Token cannot be null or empty"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } /// /// Makes an HTTP GET request to the specified endpoint. /// /// The type of the response data. /// The API endpoint. /// The cancellation token. Optional, defaults to . /// Whether to retry on 401 unauthorized responses. /// The response data. private async Task GetAsync(string endpoint, CancellationToken cancellationToken = default, bool retryOn401 = true) { return await ExecuteWithRetryAsync(async () => { using var response = await _httpClient.GetAsync($"{_baseUrl}{endpoint}", cancellationToken).ConfigureAwait(false); return await ProcessResponseAsync(response, cancellationToken).ConfigureAwait(false); }, retryOn401, cancellationToken).ConfigureAwait(false); } /// /// Makes an HTTP POST request to the specified endpoint. /// /// The type of the response data. /// The API endpoint. /// The request data. /// The cancellation token. Optional, defaults to . /// Whether to retry on 401 unauthorized responses. /// The response data. private async Task PostAsync(string endpoint, object? data, CancellationToken cancellationToken = default, bool retryOn401 = true) { return await ExecuteWithRetryAsync(async () => { var json = data != null ? JsonSerializer.Serialize(data, _jsonOptions) : string.Empty; using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var response = await _httpClient.PostAsync($"{_baseUrl}{endpoint}", content, cancellationToken).ConfigureAwait(false); return await ProcessResponseAsync(response, cancellationToken).ConfigureAwait(false); }, retryOn401, cancellationToken).ConfigureAwait(false); } /// /// Makes an HTTP PUT request to the specified endpoint. /// /// The type of the response data. /// The API endpoint. /// The request data. /// The cancellation token. Optional, defaults to . /// Whether to retry on 401 unauthorized responses. /// The response data. private async Task PutAsync(string endpoint, object? data, CancellationToken cancellationToken = default, bool retryOn401 = true) { return await ExecuteWithRetryAsync(async () => { var json = data != null ? JsonSerializer.Serialize(data, _jsonOptions) : string.Empty; using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var response = await _httpClient.PutAsync($"{_baseUrl}{endpoint}", content, cancellationToken).ConfigureAwait(false); return await ProcessResponseAsync(response, cancellationToken).ConfigureAwait(false); }, retryOn401, cancellationToken).ConfigureAwait(false); } /// /// Makes an HTTP DELETE request to the specified endpoint. /// /// The type of the response data. /// The API endpoint. /// The cancellation token. Optional, defaults to . /// Whether to retry on 401 unauthorized responses. /// The response data. private async Task DeleteAsync(string endpoint, CancellationToken cancellationToken = default, bool retryOn401 = true) { return await ExecuteWithRetryAsync(async () => { using var response = await _httpClient.DeleteAsync($"{_baseUrl}{endpoint}", cancellationToken).ConfigureAwait(false); return await ProcessResponseAsync(response, cancellationToken).ConfigureAwait(false); }, retryOn401, cancellationToken).ConfigureAwait(false); } /// /// Processes an HTTP response and handles potential errors. /// /// The type of the response data. /// The HTTP response message. /// The cancellation token. Optional, defaults to . /// The response data. private async Task ProcessResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) { if (response.StatusCode == HttpStatusCode.Unauthorized) { throw new UnauthorizedAccessException("Unauthorized access - token may be expired"); } response.EnsureSuccessStatusCode(); if (typeof(T) == typeof(Stream)) { // Response is a stream, read the response into it var ms = new MemoryStream(); var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); ms.Position = 0; // rewind return (T)(object)ms; } else { var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(content)!; } } /// /// Executes an HTTP request with retry logic for 401 unauthorized responses. /// /// The type of the response data. /// The HTTP operation to execute. /// Whether to retry on 401 unauthorized responses. /// The cancellation token. Optional, defaults to . /// The response data. private async Task ExecuteWithRetryAsync(Func> operation, bool retryOn401, CancellationToken cancellationToken = default) { const int maxRetries = 3; var attempts = 0; while (attempts <= maxRetries) { try { return await operation().ConfigureAwait(false); } catch (UnauthorizedAccessException) when (retryOn401 && attempts < maxRetries) { attempts++; await RefreshTokenAsync(cancellationToken).ConfigureAwait(false); } } // This should never be reached due to the loop logic, but included for completeness throw new UnauthorizedAccessException("Maximum retry attempts reached for token refresh"); } /// /// Refreshes the authentication token based on the credential type. /// /// The cancellation token. Optional, defaults to . private async Task RefreshTokenAsync(CancellationToken cancellationToken = default) { await _tokenRefreshSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { switch (_credentialType) { case ServerCredentialType.Password: await AcquireBearerViaPasswordAuthAsync(_credential, cancellationToken).ConfigureAwait(false); break; case ServerCredentialType.Token: await AcquireBearerViaTokenAuthAsync(_credential, cancellationToken).ConfigureAwait(false); break; default: throw new InvalidOperationException($"Unsupported credential type: {_credentialType}"); } } finally { _tokenRefreshSemaphore.Release(); } } // V1 Authentication Methods /// /// Performs password-based login to the Duplicati server (V1). /// /// The login input containing password and remember me flag. /// The cancellation token. Optional, defaults to . /// The access token output. public async Task LoginV1Async(LoginInputDto input, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/auth/login", input, cancellationToken, false).ConfigureAwait(false); } /// /// Performs token-based signin to the Duplicati server (V1). /// /// The signin input containing signin token and remember me flag. /// The cancellation token. Optional, defaults to . /// The access token output. public async Task SigninV1Async(SigninInputDto input, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/auth/signin", input, cancellationToken, false).ConfigureAwait(false); } /// /// Refreshes the access token using the refresh token (V1). /// /// The nonce to include in the refresh request. Optional, can be null. /// The cancellation token. Optional, defaults to . /// The new access token output. public async Task RefreshTokenV1Async(string? nonce, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/auth/refresh", string.IsNullOrWhiteSpace(nonce) ? null : new { Nonce = nonce }, cancellationToken, false).ConfigureAwait(false); } /// /// Issues a signin token for authentication (V1). /// /// The signin token input. /// The cancellation token. Optional, defaults to . /// The signin token output. public async Task IssueSigninTokenV1Async(IssueSigninTokenInputDto input, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/auth/issuesignintoken", input, cancellationToken).ConfigureAwait(false); } /// /// Issues a single-operation token for a specific operation (V1). /// /// The operation name. /// The cancellation token. Optional, defaults to . /// The single operation token output. public async Task IssueTokenV1Async(string operation, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/auth/issuetoken/{operation}", null, cancellationToken).ConfigureAwait(false); } /// /// Issues a forever token for long-term authentication (V1). /// /// The cancellation token. Optional, defaults to . /// The single operation token output. public async Task IssueForeverTokenV1Async(CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/auth/issue-forever-token", null, cancellationToken).ConfigureAwait(false); } /// /// Logs out and invalidates the refresh token (V1). /// /// The nonce to include in the logout request. /// The cancellation token. Optional, defaults to . /// A task representing the asynchronous operation. public async Task LogoutV1Async(string? nonce, CancellationToken cancellationToken = default) { await PostAsync("/api/v1/auth/refresh/logout", string.IsNullOrWhiteSpace(nonce) ? null : new { Nonce = nonce }, cancellationToken).ConfigureAwait(false); } // V1 Backup Management Methods /// /// Lists all backups configured on the server (V1). /// /// The cancellation token. Optional, defaults to . /// The list of backups. public async Task ListBackupsV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/backups", cancellationToken).ConfigureAwait(false); } /// /// Creates a new backup configuration (V1). /// /// The backup configuration to create. /// The cancellation token. Optional, defaults to . /// The created backup configuration. public async Task CreateBackupV1Async(BackupDto backup, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/backups", backup, cancellationToken).ConfigureAwait(false); } /// /// Imports a backup configuration (V1). /// /// The import backup input. /// The cancellation token. Optional, defaults to . /// The import backup output. public async Task ImportBackupV1Async(ImportBackupInputDto input, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/backups/import", input, cancellationToken).ConfigureAwait(false); } /// /// Gets details of a specific backup (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The backup details. public async Task GetBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}", cancellationToken).ConfigureAwait(false); } /// /// Updates a backup configuration (V1). /// /// The backup identifier. /// The updated backup configuration. /// The cancellation token. Optional, defaults to . /// The updated backup configuration. public async Task UpdateBackupV1Async(string backupId, BackupDto backup, CancellationToken cancellationToken = default) { return await PutAsync($"/api/v1/backup/{backupId}", backup, cancellationToken).ConfigureAwait(false); } /// /// Deletes a backup configuration (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The delete backup output. public async Task DeleteBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await DeleteAsync($"/api/v1/backup/{backupId}", cancellationToken).ConfigureAwait(false); } // V1 Backup Operations /// /// Starts a backup operation (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task StartBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/start", null, cancellationToken).ConfigureAwait(false); } /// /// Runs a backup operation (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task RunBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/run", null, cancellationToken).ConfigureAwait(false); } /// /// Restores files from a backup (V1). /// /// The backup identifier. /// The restore input parameters. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task RestoreBackupV1Async(string backupId, RestoreInputDto input, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/restore", input, cancellationToken).ConfigureAwait(false); } /// /// Verifies a backup's integrity (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task VerifyBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/verify", null, cancellationToken).ConfigureAwait(false); } /// /// Repairs a backup (V1). /// /// The backup identifier. /// The repair input parameters. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task RepairBackupV1Async(string backupId, RepairInputDto input, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/repair", input, cancellationToken).ConfigureAwait(false); } /// /// Repairs and updates a backup (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task RepairUpdateBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/repairupdate", null, cancellationToken).ConfigureAwait(false); } /// /// Compacts a backup (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task CompactBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/compact", null, cancellationToken).ConfigureAwait(false); } /// /// Vacuums a backup database (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task VacuumBackupV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/vacuum", null, cancellationToken).ConfigureAwait(false); } // V1 Backup Data Access /// /// Lists files in a backup (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The list of files. public async Task ListFilesV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/files", cancellationToken).ConfigureAwait(false); } /// /// Lists backup filesets (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The list of filesets. public async Task ListFilesetsV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/filesets", cancellationToken).ConfigureAwait(false); } /// /// Gets the backup log (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The backup log entries. public async Task GetBackupLogV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/log", cancellationToken).ConfigureAwait(false); } /// /// Gets the remote operation log (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The remote log entries. public async Task GetRemoteLogV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/remotelog", cancellationToken).ConfigureAwait(false); } // V1 Database Management /// /// Deletes a backup database (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task DeleteDatabaseV1Async(string backupId, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/deletedb", null, cancellationToken).ConfigureAwait(false); } /// /// Moves a backup database (V1). /// /// The backup identifier. /// The database path input. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task MoveDatabaseV1Async(string backupId, UpdateDbPathInputDto input, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/movedb", input, cancellationToken).ConfigureAwait(false); } /// /// Updates the database path (V1). /// /// The backup identifier. /// The database path input. /// The cancellation token. Optional, defaults to . /// The task started information. public async Task UpdateDatabaseV1Async(string backupId, UpdateDbPathInputDto input, CancellationToken cancellationToken = default) { return await PostAsync($"/api/v1/backup/{backupId}/updatedb", input, cancellationToken).ConfigureAwait(false); } // V1 Export Operations /// /// Exports a backup configuration (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The backup configuration. public async Task ExportBackupV1Async(string backupId, bool exportPasswords, string passPhrase, string exportToken, CancellationToken cancellationToken = default) { // [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase, [FromQuery] string token return await GetAsync($"/api/v1/backup/{backupId}/export?exportpasswords={exportPasswords}&passphrase={Uri.EscapeDataString(passPhrase)}&token={Uri.EscapeDataString(exportToken)}", cancellationToken).ConfigureAwait(false); } /// /// Exports backup as command line (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The command line export. public async Task ExportCommandlineV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/export-cmdline", cancellationToken).ConfigureAwait(false); } /// /// Exports backup arguments only (V1). /// /// The backup identifier. /// The cancellation token. Optional, defaults to . /// The arguments export. public async Task ExportArgsOnlyV1Async(string backupId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/backup/{backupId}/export-argsonly", cancellationToken).ConfigureAwait(false); } // V1 Server Management /// /// Gets the server state (V1). /// /// The cancellation token. Optional, defaults to . /// The server status. public async Task GetServerStateV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/serverstate", cancellationToken).ConfigureAwait(false); } /// /// Pauses the server (V1). /// /// The cancellation token. Optional, defaults to . /// A task representing the asynchronous operation. public async Task PauseServerV1Async(CancellationToken cancellationToken = default) { await PostAsync("/api/v1/serverstate/pause", null, cancellationToken); } /// /// Resumes the server (V1). /// /// The cancellation token. Optional, defaults to . /// A task representing the asynchronous operation. public async Task ResumeServerV1Async(CancellationToken cancellationToken = default) { await PostAsync("/api/v1/serverstate/resume", null, cancellationToken); } // V1 Task Management /// /// Lists all active tasks (V1). /// /// The cancellation token. Optional, defaults to . /// The list of active tasks. public async Task ListTasksV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/tasks", cancellationToken).ConfigureAwait(false); } /// /// Gets details of a specific task (V1). /// /// The task identifier. /// The cancellation token. Optional, defaults to . /// The task details. public async Task GetTaskV1Async(string taskId, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/task/{taskId}", cancellationToken).ConfigureAwait(false); } /// /// Stops a running task (V1). /// /// The task identifier. /// The cancellation token. Optional, defaults to . /// A task representing the asynchronous operation. public async Task StopTaskV1Async(string taskId, CancellationToken cancellationToken = default) { await PostAsync($"/api/v1/task/{taskId}/stop", null, cancellationToken); } /// /// Aborts a running task (V1). /// /// The task identifier. /// The cancellation token. Optional, defaults to . /// A task representing the asynchronous operation. public async Task AbortTaskV1Async(string taskId, CancellationToken cancellationToken = default) { await PostAsync($"/api/v1/task/{taskId}/abort", null, cancellationToken); } // V1 System Information /// /// Gets system information (V1). /// /// The cancellation token. Optional, defaults to . /// The system information. public async Task GetSystemInfoV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/systeminfo", cancellationToken).ConfigureAwait(false); } /// /// Gets the changelog (V1). /// /// The cancellation token. Optional, defaults to . /// The changelog entries. public async Task GetChangelogV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/changelog", cancellationToken).ConfigureAwait(false); } /// /// Gets license information (V1). /// /// The cancellation token. Optional, defaults to . /// The license information. public async Task GetLicensesV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/licenses", cancellationToken).ConfigureAwait(false); } /// /// Gets acknowledgements (V1). /// /// The cancellation token. Optional, defaults to . /// The acknowledgements. public async Task GetAcknowledgementsV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/acknowledgements", cancellationToken).ConfigureAwait(false); } // V1 Settings Management /// /// Gets server settings (V1). /// /// The cancellation token. Optional, defaults to . /// The server settings. public async Task GetServerSettingsV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/serversetting", cancellationToken).ConfigureAwait(false); } /// /// Updates server settings (V1). /// /// The settings to update. /// The cancellation token. Optional, defaults to . /// The updated settings. public async Task UpdateServerSettingsV1Async(SettingDto[] settings, CancellationToken cancellationToken = default) { return await PutAsync("/api/v1/serversetting", settings, cancellationToken).ConfigureAwait(false); } // V1 Filesystem Operations /// /// Browses the filesystem (V1). /// /// The cancellation token. Optional, defaults to . /// The filesystem entries. public async Task BrowseFilesystemV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/filesystem", cancellationToken).ConfigureAwait(false); } /// /// Performs filesystem operations (V1). /// /// The filesystem operation data. /// The cancellation token. Optional, defaults to . /// The operation result. public async Task PerformFilesystemOperationV1Async(object data, CancellationToken cancellationToken = default) { return await PostAsync("/api/v1/filesystem", data, cancellationToken).ConfigureAwait(false); } // V2 API Methods /// /// Lists filesets with pagination (V2). /// /// The list filesets request. /// The cancellation token. Optional, defaults to . /// The paged filesets response. public async Task> ListFilesetsV2Async(ListFilesetsRequestDto request, CancellationToken cancellationToken = default) { return await PostAsync>("/api/v2/backup/list-filesets", request, cancellationToken).ConfigureAwait(false); } /// /// Lists folder contents with pagination (V2). /// /// The list folder content request. /// The cancellation token. Optional, defaults to . /// The paged folder content response. public async Task> ListFolderContentV2Async(ListFolderContentRequestDto request, CancellationToken cancellationToken = default) { return await PostAsync>("/api/v2/backup/list-folder", request, cancellationToken).ConfigureAwait(false); } /// /// Lists file versions with pagination (V2). /// /// The list file versions request. /// The cancellation token. Optional, defaults to . /// The paged file versions response. public async Task> ListFileVersionsV2Async(ListFileVersionsRequestDto request, CancellationToken cancellationToken = default) { return await PostAsync>("/api/v2/backup/list-versions", request, cancellationToken).ConfigureAwait(false); } /// /// Searches entries with filters (V2). /// /// The search entries request. /// The cancellation token. Optional, defaults to . /// The paged search results response. public async Task> SearchEntriesV2Async(SearchEntriesRequestDto request, CancellationToken cancellationToken = default) { return await PostAsync>("/api/v2/backup/search", request, cancellationToken).ConfigureAwait(false); } /// /// Tests destination connectivity (V2). /// /// The destination test request. /// The cancellation token. Optional, defaults to . /// The destination test response. public async Task> TestDestinationV2Async(DestinationTestRequestDto request, CancellationToken cancellationToken = default) { return await PostAsync>("/api/v2/destination/test", request, cancellationToken).ConfigureAwait(false); } // V1 Folder Status Methods /// /// Gets the backup status for all source folders across all backups (V1). /// Used by the Windows Shell Extension to show overlay icons on backed up folders. /// /// The cancellation token. Optional, defaults to . /// The list of folder statuses. public async Task GetFolderStatusesV1Async(CancellationToken cancellationToken = default) { return await GetAsync("/api/v1/folderstatus", cancellationToken).ConfigureAwait(false); } /// /// Gets the backup status for a specific folder path (V1). /// Used by the Windows Shell Extension to show overlay icons on backed up folders. /// /// The folder path to check. /// The cancellation token. Optional, defaults to . /// The folder status. public async Task GetFolderStatusV1Async(string path, CancellationToken cancellationToken = default) { return await GetAsync($"/api/v1/folderstatus/{Uri.EscapeDataString(path)}", cancellationToken).ConfigureAwait(false); } /// /// Releases all resources used by the . /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_selfOwnedHttpClient) _httpClient?.Dispose(); _tokenRefreshSemaphore?.Dispose(); } _disposed = true; } }