// Copyright (C) 2026, 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.Server; using Duplicati.Server.Database; using Duplicati.Server.Serialization; using Duplicati.Server.Serialization.Interface; using Duplicati.WebserverCore.Abstractions; using Duplicati.WebserverCore.Endpoints.Shared; using Duplicati.WebserverCore.Exceptions; using Microsoft.AspNetCore.Mvc; namespace Duplicati.WebserverCore.Endpoints.V1.Backup; public class BackupPost : IEndpointV1 { public static void Map(RouteGroupBuilder group) { group.MapPost("/backup/{id}/deletedb", ([FromServices] Connection connection, [FromRoute] string id) => ExecuteDeleteDb(GetBackup(connection, id))) .RequireAuthorization(); group.MapPost("/backup/{id}/movedb", ([FromServices] Connection connection, [FromRoute] string id, [FromBody] Dto.UpdateDbPathInputDto input) => UpdateDatabasePath(connection, GetBackup(connection, id), input.path, true)) .RequireAuthorization(); group.MapPost("/backup/{id}/updatedb", ([FromServices] Connection connection, [FromRoute] string id, [FromBody] Dto.UpdateDbPathInputDto input) => UpdateDatabasePath(connection, GetBackup(connection, id), input.path, false)) .RequireAuthorization(); group.MapPost("/backup/{id}/restore", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromServices] IApplicationSettings applicationSettings, [FromRoute] string id, [FromBody] Dto.RestoreInputDto input, CancellationToken cancellationToken) => ExecuteRestore(connection, applicationSettings, id, queueRunnerService, input, cancellationToken)) .RequireAuthorization(); group.MapPost("/backup/{id}/createreport", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id) => ExecuteCreateReport(GetBackup(connection, id), queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/repair", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, Dto.RepairInputDto? input) => ExecuteRepair(GetBackup(connection, id), queueRunnerService, input)) .RequireAuthorization(); group.MapPost("/backup/{id}/repairupdate", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, Dto.RepairInputDto? input) => ExecuteRepairUpdate(GetBackup(connection, id), queueRunnerService, input)) .RequireAuthorization(); group.MapPost("/backup/{id}/vacuum", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id) => ExecuteVacuum(GetBackup(connection, id), queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/verify", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id) => ExecuteVerify(GetBackup(connection, id), queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/compact", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id) => ExecuteCompact(GetBackup(connection, id), queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/start", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, [FromQuery] bool? skipQueue) => ExecuteRunBackup(GetBackup(connection, id), skipQueue ?? false, queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/run", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id, [FromQuery] bool? skipQueue) => ExecuteRunBackup(GetBackup(connection, id), skipQueue ?? false, queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/report-remote-size", ([FromServices] Connection connection, [FromServices] IQueueRunnerService queueRunnerService, [FromRoute] string id) => ExecuteReportRemoteSize(GetBackup(connection, id), queueRunnerService)) .RequireAuthorization(); group.MapPost("/backup/{id}/copytotemp", ([FromServices] Connection connection, [FromRoute] string id) => ExecuteCopyToTemp(GetBackup(connection, id), connection)) .RequireAuthorization(); } private static IBackup GetBackup(Connection connection, string id) => connection.GetBackup(id) ?? throw new NotFoundException("Backup not found"); private static void ExecuteDeleteDb(IBackup backup) => File.Delete(backup.DBPath); private static void UpdateDatabasePath(Connection connection, IBackup backup, string targetpath, bool move) { if (string.IsNullOrWhiteSpace(targetpath)) throw new BadRequestException("No target path supplied"); if (!Path.IsPathRooted(targetpath)) throw new BadRequestException("Target path is relative, please supply a fully qualified path"); if (move && (File.Exists(targetpath) || Directory.Exists(targetpath))) throw new ConflictException("A file already exists at the new location"); if (move) File.Move(backup.DBPath, targetpath); connection.UpdateBackupDBPath(backup, targetpath); } private static async Task ExecuteRestore(Connection connection, IApplicationSettings applicationSettings, string id, IQueueRunnerService queueRunnerService, Dto.RestoreInputDto input, CancellationToken cancellationToken) { var backup = GetBackup(connection, id); var restorepath = input.restore_path; if (restorepath != null && restorepath.StartsWith("@")) { var res = await SharedRemoteOperation.ExpandUrl(connection, applicationSettings, restorepath.Substring(1), id, input.connection_string_id ?? -1, input.source_prefix, cancellationToken); if (!string.IsNullOrWhiteSpace(res.Url)) restorepath = "@" + res.Url; } return new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateRestoreTask( backup, input.paths ?? [], Library.Utility.Timeparser.ParseTimeInterval(input.time, DateTime.Now), restorepath, input.overwrite ?? false, input.permissions ?? false, input.skip_metadata ?? false, string.IsNullOrWhiteSpace(input.passphrase) ? null : input.passphrase))); } private static Dto.TaskStartedDto ExecuteCreateReport(IBackup backup, IQueueRunnerService queueRunnerService) => new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.CreateReport, backup))); private static Dto.TaskStartedDto ExecuteReportRemoteSize(IBackup backup, IQueueRunnerService queueRunnerService) => new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.ListRemote, backup))); private static Dto.TaskStartedDto ExecuteRepair(IBackup backup, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input) => DoRepair(backup, false, queueRunnerService, input); private static Dto.TaskStartedDto ExecuteRepairUpdate(IBackup backup, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input) => DoRepair(backup, true, queueRunnerService, input); private static Dto.TaskStartedDto ExecuteVacuum(IBackup backup, IQueueRunnerService queueRunnerService) => new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Vacuum, backup))); private static Dto.TaskStartedDto ExecuteVerify(IBackup backup, IQueueRunnerService queueRunnerService) => new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Verify, backup))); private static Dto.TaskStartedDto ExecuteCompact(IBackup backup, IQueueRunnerService queueRunnerService) => new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Compact, backup))); private static Dto.TaskStartedDto DoRepair(IBackup backup, bool repairUpdate, IQueueRunnerService queueRunnerService, Dto.RepairInputDto? input) { // These are all props on the input object var extra = new Dictionary(); if (input != null) { if (input.only_paths.HasValue) extra["repair-only-paths"] = input.only_paths.Value.ToString(); if (!string.IsNullOrWhiteSpace(input.time)) { extra["time"] = input.time; extra["ignore-update-if-version-exists"] = "true"; } if (!string.IsNullOrWhiteSpace(input.version)) { extra["version"] = input.version; extra["ignore-update-if-version-exists"] = "true"; } if (input.refresh_lock_info ?? false) extra["repair-refresh-lock-info"] = "true"; } var filters = input?.paths ?? []; return new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters))); } private static Dto.TaskStartedDto ExecuteRunBackup(IBackup backup, bool skipQueue, IQueueRunnerService queueRunnerService) { var t = queueRunnerService.GetCurrentTask(); var bt = t?.BackupID; // Already running if (bt != null && backup.ID == bt) return new Dto.TaskStartedDto("OK", t!.TaskID); t = queueRunnerService.GetCurrentTasks().FirstOrDefault(x => x.BackupID == backup.ID); if (t != null) return new Dto.TaskStartedDto("OK", t.TaskID); return new Dto.TaskStartedDto("OK", queueRunnerService.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), skipQueue)); } private static Dto.CreateBackupDto ExecuteCopyToTemp(IBackup backup, Connection connection) { var ipx = Serializer.Deserialize(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(backup))); using (var tf = new Library.Utility.TempFile()) ipx.SetDBPath(tf); ipx.ID = null; connection.RegisterTemporaryBackup(ipx); return new Dto.CreateBackupDto(ipx.ID, ipx.IsTemporary); } }