Files
duplicati/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs
T

204 lines
8.7 KiB
C#

using System.Text.Json;
using Duplicati.Library.IO;
using Duplicati.Library.RestAPI;
using Duplicati.Library.RestAPI.Abstractions;
using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.Server.Serialization;
using Duplicati.Server.Serialization.Interface;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Exceptions;
using Microsoft.AspNetCore.Mvc;
namespace Duplicati.WebserverCore.Endpoints.V1.Backup;
public class BackupPutDelete : IEndpointV1
{
public static void Map(RouteGroupBuilder group)
{
// TODO: Figure out why the JSON deserialization is not working here
// group.MapPut("/backup/{id}", ([FromServices] Connection connection, [FromRoute] string id, [FromBody] Dto.BackupAndScheduleInputDto input)
// => ExecutePut(connection, input);
group.MapPut("/backup/{id}", async ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id) =>
{
var opts = new JsonSerializerOptions()
{
Converters = { new DayOfWeekStringEnumConverter() }
};
var input = (await JsonSerializer.DeserializeAsync<Dto.BackupAndScheduleInputDto>(httpContextAccessor.HttpContext!.Request.Body, opts))
?? throw new BadRequestException("No data found in request body");
ExecutePut(GetBackup(connection, id), connection, input);
})
.RequireAuthorization();
group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromServices] ICaptchaProvider captchaProvider, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery(Name = "captcha-token")] string? captcha_token, [FromQuery(Name = "captcha-answer")] string? captcha_answer, [FromQuery] bool? force) =>
{
var res = ExecuteDelete(GetBackup(connection, id), workerThreadsManager, captchaProvider, liveControls, delete_remote_files ?? false, delete_local_db, captcha_token, captcha_answer, force ?? false);
if (res.Status != "OK" && httpContextAccessor.HttpContext != null)
httpContextAccessor.HttpContext.Response.StatusCode = 500;
return res;
})
.RequireAuthorization();
}
private static IBackup GetBackup(Connection connection, string id)
=> connection.GetBackup(id) ?? throw new NotFoundException("Backup not found");
private class WrappedBackup : Server.Database.Backup
{
public string? DBPathSetter
{
get => DBPath;
set => SetDBPath(value);
}
}
private static void ExecutePut(IBackup existing, Connection connection, Dto.BackupAndScheduleInputDto input)
{
// TODO: This method and "POST /backups" are 99% identical
if (input.Backup == null)
throw new BadRequestException("No backup data found in request body");
var filters = input.Backup.Filters ?? Array.Empty<Dto.BackupAndScheduleInputDto.FilterInputDto>();
var settings = input.Backup.Settings ?? Array.Empty<Dto.BackupAndScheduleInputDto.SettingInputDto>();
try
{
var backup = new WrappedBackup()
{
ID = existing.ID,
Name = input.Backup.Name,
Description = input.Backup.Description,
Tags = input.Backup.Tags,
TargetURL = input.Backup.TargetURL,
DBPathSetter = null,
Sources = input.Backup.Sources,
Settings = settings.Select(x => new Setting()
{
Name = x.Name,
Value = x.Value,
Filter = x.Filter
}).ToArray(),
Filters = filters.Select(x => new Filter()
{
Order = x.Order,
Include = x.Include,
Expression = x.Expression
}).ToArray(),
Metadata = input.Backup.Metadata ?? new Dictionary<string, string>()
};
var schedule = input.Schedule == null ? null : new Schedule()
{
ID = input.Schedule.ID,
Tags = input.Schedule.Tags,
Time = input.Schedule.Time ?? new DateTime(0),
Repeat = input.Schedule.Repeat,
LastRun = input.Schedule.LastRun ?? new DateTime(0),
Rule = input.Schedule.Rule,
AllowedDays = input.Schedule.AllowedDays
};
if (backup.IsTemporary && !existing.IsTemporary)
throw new BadRequestException("Cannot update a temporary backup with a non-temporary backup");
lock (connection.m_lock)
{
if (connection.Backups.Any(x => x.Name.Equals(backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != backup.ID))
throw new ConflictException($"There already exists a backup with the name: {backup.Name}");
var err = connection.ValidateBackup(backup, schedule);
if (!string.IsNullOrWhiteSpace(err))
throw new BadRequestException(err);
//TODO: Merge in real passwords where the placeholder is found
connection.AddOrUpdateBackupAndSchedule(backup, schedule);
}
}
catch (Exception ex)
{
if (ex is UserReportedHttpException)
throw;
throw new ServerErrorException($"Unable to save backup or schedule: {ex.Message}");
}
}
private static Dto.DeleteBackupOutputDto ExecuteDelete(IBackup backup, IWorkerThreadsManager workerThreadsManager, ICaptchaProvider captchaProvider, LiveControls liveControls, bool delete_remote_files, bool? delete_local_db, string? captcha_token, string? captcha_answer, bool force)
{
if (delete_remote_files)
{
if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer))
throw new UnauthorizedException("Missing captcha");
if (!captchaProvider.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer))
throw new ForbiddenException("Invalid captcha");
}
if (workerThreadsManager.WorkerThread!.Active)
{
try
{
//TODO: It's not safe to access the values like this,
//because the runner thread might interfere
var nt = workerThreadsManager.WorkerThread.CurrentTask;
if (backup.Equals(nt?.Backup))
{
if (!force)
return new Dto.DeleteBackupOutputDto("failed", "backup-in-progress", nt?.TaskID);
bool hasPaused = liveControls.State == LiveControls.LiveControlState.Paused;
liveControls.Pause();
for (int i = 0; i < 10; i++)
if (workerThreadsManager.WorkerThread.Active)
{
var t = workerThreadsManager.WorkerThread.CurrentTask;
if (backup.Equals(t == null ? null : t.Backup))
Thread.Sleep(1000);
else
break;
}
else
break;
if (workerThreadsManager.WorkerThread.Active)
{
var t = workerThreadsManager.WorkerThread.CurrentTask;
if (backup.Equals(t == null ? null : t.Backup))
{
if (hasPaused)
liveControls.Resume();
return new Dto.DeleteBackupOutputDto("failed", "backup-unstoppable", t?.TaskID);
}
}
if (hasPaused)
liveControls.Resume();
}
}
catch (Exception ex)
{
return new Dto.DeleteBackupOutputDto("error", ex.Message, null);
}
}
var extra = new Dictionary<string, string>();
if (delete_local_db.HasValue)
extra["delete-local-db"] = delete_local_db.Value.ToString();
if (delete_remote_files)
extra["delete-remote-files"] = "true";
return new Dto.DeleteBackupOutputDto("OK", null, workerThreadsManager.AddTask(Runner.CreateTask(DuplicatiOperation.Delete, backup, extra)));
}
}