diff --git a/Duplicati.sln b/Duplicati.sln index 639de84ea..1422c16ce 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32210.238 @@ -192,6 +192,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.WindowsMo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.WindowsModulesLoader", "Executables\net8\Duplicati.WindowsModulesLoader\Duplicati.WindowsModulesLoader.csproj", "{006167A3-64C4-40BD-AAE4-62E911ED8CBB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebserverCore.Client.UsageExample", "WebserverCore.Client.UsageExample\WebserverCore.Client.UsageExample.csproj", "{58EF2528-EA65-4940-8AFE-16D0C6E0E8BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -534,6 +536,10 @@ Global {15315ED8-1F67-478B-AFAD-59D9E5760705}.Debug|Any CPU.Build.0 = Debug|Any CPU {15315ED8-1F67-478B-AFAD-59D9E5760705}.Release|Any CPU.ActiveCfg = Release|Any CPU {15315ED8-1F67-478B-AFAD-59D9E5760705}.Release|Any CPU.Build.0 = Release|Any CPU + {58EF2528-EA65-4940-8AFE-16D0C6E0E8BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58EF2528-EA65-4940-8AFE-16D0C6E0E8BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58EF2528-EA65-4940-8AFE-16D0C6E0E8BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58EF2528-EA65-4940-8AFE-16D0C6E0E8BB}.Release|Any CPU.Build.0 = Release|Any CPU {4F0613D6-9F06-41D8-B7E8-DEEFB88DD001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F0613D6-9F06-41D8-B7E8-DEEFB88DD001}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F0613D6-9F06-41D8-B7E8-DEEFB88DD001}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -611,6 +617,7 @@ Global {8DFF553E-9D2B-4E32-BE3A-74F476159580} = {566EBBDA-19A4-4056-A615-D901D57D2439} {F760DBF2-6D4A-4934-A56C-4C0CA6758DE6} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3} {15315ED8-1F67-478B-AFAD-59D9E5760705} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} + {58EF2528-EA65-4940-8AFE-16D0C6E0E8BB} = {15388C37-9218-4818-972E-738EEA8F1602} {006167A3-64C4-40BD-AAE4-62E911ED8CBB} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/Duplicati/WebserverCore/Client/DuplicatiServerClient.cs b/Duplicati/WebserverCore/Client/DuplicatiServerClient.cs new file mode 100644 index 000000000..c713c302b --- /dev/null +++ b/Duplicati/WebserverCore/Client/DuplicatiServerClient.cs @@ -0,0 +1,884 @@ +// 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 cancellation token. Optional, defaults to . + /// The new access token output. + public async Task RefreshTokenV1Async(CancellationToken cancellationToken = default) + { + return await PostAsync("/api/v1/auth/refresh", null, 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 cancellation token. Optional, defaults to . + /// A task representing the asynchronous operation. + public async Task LogoutV1Async(CancellationToken cancellationToken = default) + { + await PostAsync("/api/v1/auth/refresh/logout", null, 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); + } + + /// + /// 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; + } +} diff --git a/Duplicati/WebserverCore/Client/ServerCredentialType.cs b/Duplicati/WebserverCore/Client/ServerCredentialType.cs new file mode 100644 index 000000000..f456ddcda --- /dev/null +++ b/Duplicati/WebserverCore/Client/ServerCredentialType.cs @@ -0,0 +1,19 @@ +namespace Duplicati.WebserverCore.Client; + +/// +/// Specifies the type of credential used for authenticating with the Duplicati server. +/// +public enum ServerCredentialType +{ + /// + /// Use password-based authentication. The credential should be the server's webserver password. + /// This will perform a login operation via the /api/v1/auth/login endpoint. + /// + Password, + + /// + /// Use token-based authentication. The credential should be a signin token obtained from the server. + /// This will perform a signin operation via the /api/v1/auth/signin endpoint. + /// + Token +} diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs index 875b21a29..96d5e2fa6 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs @@ -33,7 +33,7 @@ namespace Duplicati.WebserverCore.Endpoints.V1.Backup; public class BackupGet : IEndpointV1 { - private record GetBackupResultDto(Dto.ScheduleDto? Schedule, Dto.BackupDto Backup, Dictionary DisplayNames); + public record GetBackupResultDto(Dto.ScheduleDto? Schedule, Dto.BackupDto Backup, Dictionary DisplayNames); public static void Map(RouteGroupBuilder group) { diff --git a/WebserverCore.Client.UsageExample/Program.cs b/WebserverCore.Client.UsageExample/Program.cs new file mode 100644 index 000000000..feb749c53 --- /dev/null +++ b/WebserverCore.Client.UsageExample/Program.cs @@ -0,0 +1,628 @@ +// 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 Duplicati.WebserverCore.Client; +using Duplicati.WebserverCore.Dto; +using Duplicati.WebserverCore.Dto.V2; + +class Program +{ + static async Task Main(string[] args) + { + var serverUrl = "http://localhost:8200"; + + var password = Environment.GetEnvironmentVariable("TEST_PASSWORD") ?? + throw new InvalidOperationException("Must set the TEST_PASSWORD environment variable"); + + using var client = new DuplicatiServerClient(serverUrl, ServerCredentialType.Password, password); + + try + { + // Authenticate explicitly (optional - happens automatically on first API call) + await client.Authenticate(); + Console.WriteLine("✓ Authentication successful"); + + // Demonstrate all client methods + await DemonstrateAuthenticationMethods(client); + await DemonstrateBackupManagement(client); + await DemonstrateBackupOperations(client); + await DemonstrateBackupDataAccess(client); + await DemonstrateDatabaseManagement(client); + await DemonstrateExportOperations(client); + await DemonstrateServerManagement(client); + await DemonstrateTaskManagement(client); + await DemonstrateSystemInformation(client); + await DemonstrateSettingsManagement(client); + await DemonstrateFilesystemOperations(client); + await DemonstrateV2ApiMethods(client); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Error: {ex.Message}"); + } + } + + /// + /// Demonstrates authentication-related methods + /// + static async Task DemonstrateAuthenticationMethods(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Authentication Methods ==="); + + try + { + // Issue a signin token + var signinTokenResult = await client.IssueSigninTokenV1Async( + new IssueSigninTokenInputDto( Environment.GetEnvironmentVariable("TEST_PASSWORD") ) { }, CancellationToken.None); + Console.WriteLine($"✓ Signin token issued: {signinTokenResult.Token[..10]}..."); + + // Issue a single operation token + var operationToken = await client.IssueTokenV1Async("export", CancellationToken.None); + Console.WriteLine($"✓ Operation token issued: {operationToken.Token[..10]}..."); + + await client.Authenticate(); + // Issue a forever token + try + { + var foreverToken = await client.IssueForeverTokenV1Async(CancellationToken.None); + Console.WriteLine($"✓ Forever token issued: {foreverToken.Token[..10]}..."); + } + catch (Exception e) + { + // expected here if foreever tokens are disabled + } + + // Refresh token (if using refresh tokens) + try + { + var refreshResult = await client.RefreshTokenV1Async(CancellationToken.None); + Console.WriteLine($"✓ Token refreshed: {refreshResult.AccessToken[..10]}..."); + } + catch (Exception ex) + { + Console.WriteLine($"ℹ️ Token refresh not available: {ex.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Authentication demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates backup management methods + /// + static async Task DemonstrateBackupManagement(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Backup Management ==="); + + try + { + // List all backups + var backups = await client.ListBackupsV1Async(CancellationToken.None); + Console.WriteLine($"✓ Found {backups.Length} backups"); + + foreach (var backup in backups) + { + Console.WriteLine($" - {backup.Backup.Name} (ID: {backup.Backup.ID})"); + + // Get detailed backup information + var backupDetails = await client.GetBackupV1Async(backup.Backup.ID, CancellationToken.None); + Console.WriteLine($" Name: {backupDetails.Backup.Name}"); + Console.WriteLine($" Sources: {string.Join(", ", backupDetails.Backup.Sources ?? [])}"); + + + var exportToken = await client.IssueTokenV1Async("export", CancellationToken.None); + // Export backup configuration + var exportedBackup = await client.ExportBackupV1Async(backup.Backup.ID, true, "random", exportToken.Token, CancellationToken.None); + Console.WriteLine($" ✓ Backup configuration exported the stream is {exportedBackup.Length} bytes long"); + + // Export as command line + var cmdlineExport = await client.ExportCommandlineV1Async(backup.Backup.ID, CancellationToken.None); + Console.WriteLine($" ✓ Command line exported: {cmdlineExport.Command[..50]}..."); + + // Export arguments only + var argsExport = await client.ExportArgsOnlyV1Async(backup.Backup.ID, CancellationToken.None); + Console.WriteLine($" ✓ Arguments exported: {argsExport.Arguments.ToArray().Length} args"); + } + + // Demonstrate backup creation (commented out to avoid creating test backups) + /* + var newBackup = new BackupDto + { + Name = "Test Backup", + Description = "Created via API example", + TargetURL = "file:///tmp/test-backup", + Sources = ["~/Documents"], + Settings = new Dictionary + { + { "compression-module", "zip" }, + { "encryption-module", "aes" } + } + }; + + var createdBackup = await client.CreateBackupV1Async(newBackup, CancellationToken.None); + Console.WriteLine($"✓ Created backup: {createdBackup.Name}"); + + // Update the backup + createdBackup.Description = "Updated via API"; + var updatedBackup = await client.UpdateBackupV1Async(createdBackup.ID, createdBackup, CancellationToken.None); + Console.WriteLine($"✓ Updated backup: {updatedBackup.Description}"); + + // Delete the backup + var deleteResult = await client.DeleteBackupV1Async(createdBackup.ID, CancellationToken.None); + Console.WriteLine($"✓ Deleted backup: {deleteResult.DeletedFileCount} files deleted"); + */ + } + catch (Exception ex) + { + Console.WriteLine($"❌ Backup management demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates backup operations + /// + static async Task DemonstrateBackupOperations(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Backup Operations ==="); + + try + { + var backups = await client.ListBackupsV1Async(CancellationToken.None); + if (backups.Length == 0) + { + Console.WriteLine("ℹ️ No backups found to demonstrate operations"); + return; + } + + var firstBackup = backups[0]; + Console.WriteLine($"Demonstrating operations on backup: {firstBackup.Backup.Name}"); + + // Start a backup (commented out to avoid actual backup operations) + /* + var startResult = await client.StartBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Backup started: Task ID {startResult.TaskID}"); + + // Run a backup + var runResult = await client.RunBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Backup run: Task ID {runResult.TaskID}"); + + // Verify backup + var verifyResult = await client.VerifyBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Backup verification started: Task ID {verifyResult.TaskID}"); + + // Compact backup + var compactResult = await client.CompactBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Backup compaction started: Task ID {compactResult.TaskID}"); + + // Vacuum backup database + var vacuumResult = await client.VacuumBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Database vacuum started: Task ID {vacuumResult.TaskID}"); + + // Repair backup + var repairInput = new RepairInputDto { OnlyLogErrors = true }; + var repairResult = await client.RepairBackupV1Async(firstBackup.Backup.ID, repairInput, CancellationToken.None); + Console.WriteLine($"✓ Backup repair started: Task ID {repairResult.TaskID}"); + + // Repair and update backup + var repairUpdateResult = await client.RepairUpdateBackupV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Backup repair/update started: Task ID {repairUpdateResult.TaskID}"); + + // Restore files + var restoreInput = new RestoreInputDto + { + Path = "~/Documents/test.txt", + RestoreLocation = "~/Desktop/restored_test.txt" + }; + var restoreResult = await client.RestoreBackupV1Async(firstBackup.Backup.ID, restoreInput, CancellationToken.None); + Console.WriteLine($"✓ File restoration started: Task ID {restoreResult.TaskID}"); + */ + + Console.WriteLine("ℹ️ Backup operations are commented out to avoid actual operations"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Backup operations demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates backup data access methods + /// + static async Task DemonstrateBackupDataAccess(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Backup Data Access ==="); + + try + { + var backups = await client.ListBackupsV1Async(CancellationToken.None); + if (backups.Length == 0) + { + Console.WriteLine("ℹ️ No backups found to demonstrate data access"); + return; + } + + var firstBackup = backups[0]; + Console.WriteLine($"Accessing data for backup: {firstBackup.Backup.Name}"); + + // List files in backup + var files = await client.ListFilesV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Found {files.Length} files/folders in backup"); + + // List filesets + var filesets = await client.ListFilesetsV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Found {filesets.Length} filesets"); + + // Get backup log + var backupLog = await client.GetBackupLogV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Retrieved backup log: {backupLog.Length} entries"); + + // Get remote log + var remoteLog = await client.GetRemoteLogV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Retrieved remote log: {remoteLog.Length} entries"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Backup data access demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates database management methods + /// + static async Task DemonstrateDatabaseManagement(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Database Management ==="); + + try + { + var backups = await client.ListBackupsV1Async(CancellationToken.None); + if (backups.Length == 0) + { + Console.WriteLine("ℹ️ No backups found to demonstrate database management"); + return; + } + + var firstBackup = backups[0]; + Console.WriteLine($"Database management for backup: {firstBackup.Backup.Name}"); + + // Database operations are commented out to avoid destructive actions + /* + // Move database + var moveDbInput = new UpdateDbPathInputDto { Path = "/tmp/new_db_location" }; + var moveResult = await client.MoveDatabaseV1Async(firstBackup.Backup.ID, moveDbInput, CancellationToken.None); + Console.WriteLine($"✓ Database move started: Task ID {moveResult.TaskID}"); + + // Update database path + var updateDbInput = new UpdateDbPathInputDto { Path = "/tmp/updated_db_location" }; + var updateResult = await client.UpdateDatabaseV1Async(firstBackup.Backup.ID, updateDbInput, CancellationToken.None); + Console.WriteLine($"✓ Database update started: Task ID {updateResult.TaskID}"); + + // Delete database + var deleteDbResult = await client.DeleteDatabaseV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Database deletion started: Task ID {deleteDbResult.TaskID}"); + */ + + Console.WriteLine("ℹ️ Database operations are commented out to avoid destructive actions"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Database management demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates export operations + /// + static async Task DemonstrateExportOperations(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Export Operations ==="); + + try + { + var backups = await client.ListBackupsV1Async(CancellationToken.None); + if (backups.Length == 0) + { + Console.WriteLine("ℹ️ No backups found to demonstrate export operations"); + return; + } + + var firstBackup = backups[0]; + Console.WriteLine($"Export operations for backup: {firstBackup.Backup.Name}"); + + // Export backup configuration + var exportToken = await client.IssueTokenV1Async("export", CancellationToken.None); + var exportedConfig = await client.ExportBackupV1Async(firstBackup.Backup.ID,true, "random", exportToken.Token, CancellationToken.None); + Console.WriteLine($"✓ Exported backup configuration: {exportedConfig}"); + + // Export as command line + var cmdlineExport = await client.ExportCommandlineV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Exported command line: {cmdlineExport.Command.Length} characters"); + + // Export arguments only + var argsExport = await client.ExportArgsOnlyV1Async(firstBackup.Backup.ID, CancellationToken.None); + Console.WriteLine($"✓ Exported arguments: {argsExport.Arguments.ToArray().Length} arguments"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Export operations demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates server management methods + /// + static async Task DemonstrateServerManagement(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Server Management ==="); + + try + { + // Get server state + var serverState = await client.GetServerStateV1Async(CancellationToken.None); + Console.WriteLine($"✓ Server state: {serverState.ProgramState}"); + Console.WriteLine($" - Active task: {serverState.ActiveTask?.Item2 ?? "None"}"); + Console.WriteLine($" - Scheduled tasks: {serverState.SchedulerQueueIds?.Count ?? 0}"); + + // Server control operations are commented out to avoid disrupting the server + /* + // Pause server + await client.PauseServerV1Async(CancellationToken.None); + Console.WriteLine("✓ Server paused"); + + // Resume server + await client.ResumeServerV1Async(CancellationToken.None); + Console.WriteLine("✓ Server resumed"); + */ + + Console.WriteLine("ℹ️ Server control operations are commented out to avoid disruption"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Server management demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates task management methods + /// + static async Task DemonstrateTaskManagement(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Task Management ==="); + + try + { + // List active tasks + var tasks = await client.ListTasksV1Async(CancellationToken.None); + Console.WriteLine($"✓ Found {tasks.Length} active tasks"); + + // If there are tasks, get details of the first one + if (tasks.Length > 0 && tasks[0] is System.Text.Json.JsonElement taskElement) + { + if (taskElement.TryGetProperty("TaskID", out var taskIdProperty)) + { + var taskId = taskIdProperty.GetString(); + if (!string.IsNullOrEmpty(taskId)) + { + var taskDetails = await client.GetTaskV1Async(taskId, CancellationToken.None); + Console.WriteLine($"✓ Task details: {taskDetails.Status}"); + + // Task control operations are commented out to avoid disrupting running tasks + /* + // Stop task + await client.StopTaskV1Async(taskId, CancellationToken.None); + Console.WriteLine($"✓ Task {taskId} stopped"); + + // Abort task + await client.AbortTaskV1Async(taskId, CancellationToken.None); + Console.WriteLine($"✓ Task {taskId} aborted"); + */ + } + } + } + + Console.WriteLine("ℹ️ Task control operations are commented out to avoid disruption"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Task management demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates system information methods + /// + static async Task DemonstrateSystemInformation(DuplicatiServerClient client) + { + Console.WriteLine("\n=== System Information ==="); + + try + { + // Get system information + var systemInfo = await client.GetSystemInfoV1Async(CancellationToken.None); + Console.WriteLine($"✓ System Info:"); + Console.WriteLine($" - Version: {systemInfo.ServerVersionName}"); + Console.WriteLine($" - Server Version: {systemInfo.ServerVersion}"); + Console.WriteLine($" - Machine Name: {systemInfo.MachineName}"); + Console.WriteLine($" - User Name: {systemInfo.UserName}"); + Console.WriteLine($" - OS Name: {systemInfo.OSType}"); + Console.WriteLine($" - .NET Version: {systemInfo.CLRVersion}"); + + // Get changelog + var changelog = await client.GetChangelogV1Async(CancellationToken.None); + Console.WriteLine($"✓ Changelog: {changelog.Length} entries"); + + // Get licenses + var licenses = await client.GetLicensesV1Async(CancellationToken.None); + Console.WriteLine($"✓ Licenses: {licenses.Length} licenses"); + + // Get acknowledgements + var acknowledgements = await client.GetAcknowledgementsV1Async(CancellationToken.None); + Console.WriteLine($"✓ Acknowledgements: {acknowledgements.Length} acknowledgements"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ System information demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates settings management methods + /// + static async Task DemonstrateSettingsManagement(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Settings Management ==="); + + try + { + // Get server settings + var settings = await client.GetServerSettingsV1Async(CancellationToken.None); + Console.WriteLine($"✓ Retrieved {settings.Length} server settings"); + + foreach (var setting in settings.Take(5)) // Show first 5 settings + { + Console.WriteLine($" - {setting.Name}: {setting.Value}"); + } + + // Settings update is commented out to avoid changing server configuration + /* + // Update server settings + var settingsToUpdate = new[] + { + new SettingDto { Name = "example-setting", Value = "example-value" } + }; + var updatedSettings = await client.UpdateServerSettingsV1Async(settingsToUpdate, CancellationToken.None); + Console.WriteLine($"✓ Updated {updatedSettings.Length} settings"); + */ + + Console.WriteLine("ℹ️ Settings updates are commented out to avoid changing configuration"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Settings management demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates filesystem operations + /// + static async Task DemonstrateFilesystemOperations(DuplicatiServerClient client) + { + Console.WriteLine("\n=== Filesystem Operations ==="); + + try + { + // Browse filesystem + var fsEntries = await client.BrowseFilesystemV1Async(CancellationToken.None); + Console.WriteLine($"✓ Found {fsEntries.Length} filesystem entries"); + + foreach (var entry in fsEntries.Take(5)) // Show first 5 entries + { + Console.WriteLine($" - {entry.text} Size: ({entry.fileSize})"); + } + + // Filesystem operations are commented out to avoid file system changes + /* + // Perform filesystem operation + var fsOperation = new { operation = "list", path = "/" }; + var fsResult = await client.PerformFilesystemOperationV1Async(fsOperation, CancellationToken.None); + Console.WriteLine($"✓ Filesystem operation completed"); + */ + + Console.WriteLine("ℹ️ Filesystem operations are commented out to avoid file system changes"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Filesystem operations demo error: {ex.Message}"); + } + } + + /// + /// Demonstrates V2 API methods + /// + static async Task DemonstrateV2ApiMethods(DuplicatiServerClient client) + { + Console.WriteLine("\n=== V2 API Methods ==="); + + try + { + var backups = await client.ListBackupsV1Async(CancellationToken.None); + if (backups.Length == 0) + { + Console.WriteLine("ℹ️ No backups found to demonstrate V2 API methods"); + return; + } + + var firstBackup = backups[0]; + Console.WriteLine($"V2 API demonstrations for backup: {firstBackup.Backup.Name}"); + + // List filesets with pagination + var filesetsRequest = new ListFilesetsRequestDto + { + BackupId = firstBackup.Backup.ID + }; + var filesetsResponse = await client.ListFilesetsV2Async(filesetsRequest, CancellationToken.None); + Console.WriteLine($"✓ V2 Filesets: {filesetsResponse.Data.ToArray().Length} items"); + + // List folder content with pagination + var folderRequest = new ListFolderContentRequestDto + { + BackupId = firstBackup.Backup.ID, + PageSize = 10, + Paths = null, + Time = null, + Page = null + }; + var folderResponse = await client.ListFolderContentV2Async(folderRequest, CancellationToken.None); + Console.WriteLine($"✓ V2 Folder Content: {folderResponse.Data.ToArray().Length} items,"); + + // Search entries + var searchRequest = new SearchEntriesRequestDto + { + BackupId = firstBackup.Backup.ID, + Filters = ["*"], + PageSize = 10, + Paths = null, + Time = null, + Page = 0 + }; + var searchResponse = await client.SearchEntriesV2Async(searchRequest, CancellationToken.None); + Console.WriteLine($"✓ V2 Search Results: {searchResponse.Data.ToArray().Length} items"); + + // Test destination (commented out as it requires valid destination configuration) + /* + var destTestRequest = new DestinationTestRequestDto + { + BackupId = firstBackup.Backup.ID + }; + var destTestResponse = await client.TestDestinationV2Async(destTestRequest, CancellationToken.None); + Console.WriteLine($"✓ V2 Destination Test: {destTestResponse.Data.Success}"); + */ + + Console.WriteLine("ℹ️ Destination test is commented out as it requires valid configuration"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ V2 API methods demo error: {ex.Message}"); + } + } +} diff --git a/WebserverCore.Client.UsageExample/WebserverCore.Client.UsageExample.csproj b/WebserverCore.Client.UsageExample/WebserverCore.Client.UsageExample.csproj new file mode 100644 index 000000000..a6bc0be37 --- /dev/null +++ b/WebserverCore.Client.UsageExample/WebserverCore.Client.UsageExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + +