Files
duplicati/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs
T
Kenneth Skovhede 27f0bb4ea0 Unified code to find the data folder.
The `machineid.txt` and `installation.txt` files are now default located in same folder where the database is default located.
2025-01-27 22:51:45 +01:00

313 lines
14 KiB
C#

// 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.Text.Json;
using System.Text.Json.Serialization;
using Duplicati.Library.Interface;
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 BackupGet : IEndpointV1
{
private record GetBackupResultDto(Dto.ScheduleDto? Schedule, Dto.BackupDto Backup, Dictionary<string, string> DisplayNames);
public static void Map(RouteGroupBuilder group)
{
group.MapGet("/backup/{id}", ([FromServices] Connection connection, [FromRoute] string id)
=> ExecuteGet(connection, GetBackup(connection, id)))
.RequireAuthorization();
group.MapGet("/backup/{id}/files", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] string? filter, [FromQuery] string? time, [FromQuery(Name = "all-versions")] bool? allVersions, [FromQuery(Name = "prefix-only")] bool? prefixOnly, [FromQuery(Name = "folder-contents")] bool? folderContents)
=> ExecuteGetFiles(GetBackup(connection, id), filter, time, allVersions ?? false, prefixOnly ?? false, folderContents ?? false, new Dictionary<string, string>()))
.RequireAuthorization();
group.MapGet("/backup/{id}/log", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize)
=> ExecuteGetLog(connection, GetBackup(connection, id), offset, pagesize ?? 100))
.RequireAuthorization();
group.MapGet("/backup/{id}/remotelog", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery] long? offset, [FromQuery] long? pagesize)
=> ExecuteGetRemotelog(connection, GetBackup(connection, id), offset, pagesize ?? 100))
.RequireAuthorization();
group.MapGet("/backup/{id}/filesets", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "include-metadata")] bool? includeMetadata, [FromQuery(Name = "from-remote-only")] bool? fromRemoteOnly)
=> ExecuteGetFilesets(GetBackup(connection, id), includeMetadata ?? false, fromRemoteOnly ?? false))
.RequireAuthorization();
group.MapGet("/backup/{id}/export-argsonly", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
=> ExecuteGetExportArgsOnly(GetBackup(connection, id), exportPasswords ?? false))
.RequireAuthorization();
group.MapGet("/backup/{id}/export-cmdline", ([FromServices] Connection connection, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase)
=> ExecuteGetExportCmdline(GetBackup(connection, id), exportPasswords ?? false))
.RequireAuthorization();
group.MapGet("/backup/{id}/export", ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IJWTTokenProvider jWTTokenProvider, [FromRoute] string id, [FromQuery(Name = "export-passwords")] bool? exportPasswords, [FromQuery] string? passphrase, [FromQuery] string token, CancellationToken ct) =>
{
// Custom authorization check
var singleOperationToken = jWTTokenProvider.ReadSingleOperationToken(token);
if (singleOperationToken.Operation != "export")
throw new UnauthorizedException("Invalid operation");
var (data, filename) = ExecuteGetExport(connection, GetBackup(connection, id), exportPasswords ?? false, passphrase);
var resp = httpContextAccessor.HttpContext!.Response;
resp.ContentLength = data.Length;
resp.ContentType = "application/octet-stream";
resp.Headers.Append("Content-Disposition", $"attachment; filename={filename}");
resp.Body.WriteAsync(data, ct);
});
group.MapGet("/backup/{id}/isdbusedelsewhere", ([FromServices] Connection connection, [FromRoute] string id)
=> ExecuteGetIsdbUsedElsewhere(GetBackup(connection, id)))
.RequireAuthorization();
group.MapGet("/backup/{id}/isactive", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromRoute] string id)
=> ExecuteGetIsActive(workerThreadsManager, GetBackup(connection, id)))
.RequireAuthorization();
}
private static IBackup GetBackup(Connection connection, string id)
=> connection.GetBackup(id) ?? throw new NotFoundException("Backup not found");
private static GetBackupResultDto ExecuteGet(Connection connection, IBackup bk)
{
var scheduleId = connection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
var schedule = scheduleId.Any() ? connection.GetSchedule(scheduleId.First()) : null;
var sourcenames = SpecialFolders.GetSourceNames(bk);
//TODO: Filter out the password in both settings and the target url
return new GetBackupResultDto(
schedule == null ? null : new Dto.ScheduleDto()
{
ID = schedule.ID,
Tags = schedule.Tags,
Time = schedule.Time,
Repeat = schedule.Repeat,
LastRun = schedule.LastRun,
Rule = schedule.Rule,
AllowedDays = schedule.AllowedDays
},
new Dto.BackupDto()
{
ID = bk.ID,
Name = bk.Name,
Sources = bk.Sources,
Settings = bk.Settings?.Select(x => new Dto.SettingDto()
{
Name = x.Name,
Value = x.Value,
Filter = x.Filter,
Argument = x.Argument
}).ToArray(),
Filters = bk.Filters?.Select(x => new Dto.FilterDto()
{
Order = x.Order,
Include = x.Include,
Expression = x.Expression
}).ToArray(),
Metadata = bk.Metadata,
Description = bk.Description,
Tags = bk.Tags,
TargetURL = bk.TargetURL,
DBPath = bk.DBPath,
IsTemporary = bk.IsTemporary,
IsUnencryptedOrPassphraseStored = false,
},
sourcenames
);
}
private static Dictionary<string, object> SearchFiles(IBackup backup, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
{
if (string.IsNullOrWhiteSpace(timestring) && !allVersions)
throw new BadRequestException("Invalid or missing time");
var time = new DateTime();
if (!allVersions)
time = Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
var r = Runner.Run(Runner.CreateListTask(backup, [filter], prefixOnly, allVersions, folderContents, time), false) as Duplicati.Library.Interface.IListResults;
if (r == null)
throw new ServerErrorException("No result from list operation");
var result = new Dictionary<string, object>();
foreach (var k in extraValues)
result[k.Key] = k.Value;
result["Filesets"] = r.Filesets;
result["Files"] = r.Files
// Group directories first - support either directory separator here as we may be restoring data from an alternate platform
.OrderByDescending(f => (f.Path.StartsWith('/') && f.Path.EndsWith('/')) || (!f.Path.StartsWith('/') && f.Path.EndsWith('\\')))
// Sort both groups (directories and files) alphabetically
.ThenBy(f => f.Path);
return result;
}
private static Dictionary<string, object> ExecuteGetFiles(IBackup bk, string? filter, string? timestring, bool allVersions, bool prefixOnly, bool folderContents, Dictionary<string, string> extraValues)
=> SearchFiles(bk, filter, timestring, allVersions, prefixOnly, folderContents, extraValues);
private static List<Dictionary<string, object>> ExecuteGetLog(Connection connection, IBackup bk, long? offset, long pagesize)
{
if (!File.Exists(bk.DBPath))
return new List<Dictionary<string, object>>();
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
using (var cmd = con.CreateCommand())
return LogData.DumpTable(cmd, "LogData", "ID", offset, pagesize);
}
private static List<Dictionary<string, object>> ExecuteGetRemotelog(Connection connection, IBackup bk, long? offset, long pagesize)
{
if (!File.Exists(bk.DBPath))
return new List<Dictionary<string, object>>();
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
using (var cmd = con.CreateCommand())
{
var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", offset, pagesize);
// Unwrap raw data to a string
foreach (var n in dt)
try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); }
catch { }
return dt;
}
}
private static IEnumerable<IListResultFileset> ExecuteGetFilesets(IBackup bk, bool includeMetadata, bool fromRemoteOnly)
{
var extra = new Dictionary<string, string>
{
["list-sets-only"] = "true"
};
if (includeMetadata)
extra["list-sets-only"] = "false";
if (fromRemoteOnly)
extra["no-local-db"] = "true";
var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, bk, extra), false) as IListResults;
if (r == null)
throw new ServerErrorException("No result from list operation");
if (r.EncryptedFiles && bk.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase)))
throw new ServerErrorException("encrypted-storage");
return r.Filesets;
}
public static void RemovePasswords(IBackup backup)
{
backup.SanitizeSettings();
backup.SanitizeTargetUrl();
}
private static Dto.ExportCommandlineDto ExecuteGetExportCmdline(IBackup backup, bool exportPasswords)
{
if (!exportPasswords)
RemovePasswords(backup);
return new Dto.ExportCommandlineDto(Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)));
}
private static Dto.ExportArgsOnlyDto ExecuteGetExportArgsOnly(IBackup backup, bool exportPasswords)
{
if (!exportPasswords)
RemovePasswords(backup);
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
return new Dto.ExportArgsOnlyDto(
parts.First(),
parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
);
}
private static (byte[] Data, string Filename) ExecuteGetExport(Connection connection, IBackup backup, bool exportPasswords, string? passphrase)
{
if (!exportPasswords)
RemovePasswords(backup);
byte[] data = EncodeDataForExport(connection, backup, passphrase);
string filename = Uri.EscapeDataString(backup.Name + "-duplicati-config.json");
if (!string.IsNullOrWhiteSpace(passphrase))
filename += ".aes";
return (data, filename);
}
private static byte[] EncodeDataForExport(Connection connection, IBackup bk, string? passphrase)
{
var ipx = connection.PrepareBackupForExport(bk);
using var ms = new MemoryStream();
JsonSerializer.Serialize(ms, ipx, new JsonSerializerOptions()
{
WriteIndented = true,
Converters = {
new JsonStringEnumConverter(),
new DayOfWeekStringEnumConverter()
}
});
if (string.IsNullOrWhiteSpace(passphrase))
return ms.ToArray();
ms.Position = 0;
using var ms2 = new MemoryStream();
using (var m = new Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
m.Encrypt(ms, ms2);
return ms2.ToArray();
}
private static Dto.IsDbUsedElsewhereDto ExecuteGetIsdbUsedElsewhere(IBackup bk)
=> new Dto.IsDbUsedElsewhereDto(Library.Main.CLIDatabaseLocator.IsDatabasePathInUse(bk.DBPath));
private static Dto.IsBackupActiveDto ExecuteGetIsActive(IWorkerThreadsManager workerThreadsManager, IBackup bk)
{
if (workerThreadsManager.WorkerThread == null)
throw new InvalidOperationException("Worker thread not available");
var t = workerThreadsManager.WorkerThread.CurrentTask;
var bt = t?.Backup;
if (bt != null && bk.ID == bt.ID)
return new Dto.IsBackupActiveDto("OK", true);
if (workerThreadsManager.WorkerThread.CurrentTasks.Any(x => x?.Backup == null || x.Backup.ID == bk.ID))
return new Dto.IsBackupActiveDto("OK", true);
return new Dto.IsBackupActiveDto("OK", false);
}
}