Store configuration with backup
This PR revives the `store-task-config` option that was never shown in the user interface and extends the feature to allow more flexibility in exporting the configurations. The `Auto` setting is now on by default. For encrypted backups, this will store the backup configuration of the current backup with the backup for easy restore of a configuration. For unencrypted backups, nothing will be stored by default. To manually pick the what backup configurations are stored, the following options are also available: - None: no configuration is stored - Self: The configuration of the current backup is stored - All: The configurations of all backups are stored If the backup is not encrypted, the data is stored without any secrets (encryption keys, passphrases, passwords, api-keys, etc). To override this, the following two options are also present: - SelfWithForcedSecrets - AllWithForcedSecrets Using one of these when encryption is enabled has no additional effects, but for unecrypted backups this will include all secrets in the backups in plain-text. This fixes #6256 This fixes #3073
This commit is contained in:
@@ -184,6 +184,104 @@ namespace Duplicati.Server
|
||||
return new CustomRunnerTask(runner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the raw option value into a <see cref="StoreTaskConfigMode"/>.
|
||||
/// For backwards compatibility, boolean true is treated as <see cref="StoreTaskConfigMode.Self"/>,
|
||||
/// boolean false as <see cref="StoreTaskConfigMode.None"/>, and null/whitespace as <see cref="StoreTaskConfigMode.Auto"/>.
|
||||
/// </summary>
|
||||
/// <param name="rawValue">The raw option value.</param>
|
||||
/// <returns>The parsed mode.</returns>
|
||||
public static StoreTaskConfigMode ParseStoreTaskConfigMode(string? rawValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
return StoreTaskConfigMode.Auto;
|
||||
|
||||
// Handle boolean values for backwards compatibility, use a lambda capture to see if we hit the default case
|
||||
var usedDefault = false;
|
||||
var parsedBool = Utility.ParseBool(rawValue, () => { usedDefault = true; return false; });
|
||||
if (!usedDefault)
|
||||
return parsedBool ? StoreTaskConfigMode.Self : StoreTaskConfigMode.None;
|
||||
|
||||
return Utility.ParseEnum(rawValue, StoreTaskConfigMode.Auto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the backup is configured to use encryption.
|
||||
/// </summary>
|
||||
/// <param name="options">The backup options dictionary.</param>
|
||||
/// <returns>True if encryption is enabled; otherwise, false.</returns>
|
||||
private static bool IsBackupEncryptionEnabled(Dictionary<string, string?> options)
|
||||
{
|
||||
// In principle, this check should be enough
|
||||
if (Utility.ParseBoolOption(options, "no-encryption"))
|
||||
return false;
|
||||
|
||||
// But since we explicitly set the encryption module, we also check that this is set
|
||||
if (string.IsNullOrWhiteSpace(options.GetValueOrDefault("encryption-module")))
|
||||
return false;
|
||||
|
||||
// Also, we need a passphrase
|
||||
if (string.IsNullOrWhiteSpace(options.GetValueOrDefault("passphrase")))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective store-task-config mode based on encryption settings.
|
||||
/// </summary>
|
||||
/// <param name="mode">The requested mode.</param>
|
||||
/// <param name="encryptionEnabled">Whether backup encryption is enabled.</param>
|
||||
/// <returns>The effective mode to use.</returns>
|
||||
private static StoreTaskConfigMode ResolveEffectiveMode(StoreTaskConfigMode mode, bool encryptionEnabled)
|
||||
{
|
||||
if (encryptionEnabled)
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
StoreTaskConfigMode.Auto => StoreTaskConfigMode.SelfWithForcedSecrets,
|
||||
StoreTaskConfigMode.Self => StoreTaskConfigMode.SelfWithForcedSecrets,
|
||||
StoreTaskConfigMode.All => StoreTaskConfigMode.AllWithForcedSecrets,
|
||||
_ => mode
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
StoreTaskConfigMode.Auto => StoreTaskConfigMode.None,
|
||||
StoreTaskConfigMode.Self => StoreTaskConfigMode.Self,
|
||||
StoreTaskConfigMode.All => StoreTaskConfigMode.All,
|
||||
_ => mode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a backup for export, optionally removing sensitive information.
|
||||
/// </summary>
|
||||
/// <param name="databaseConnection">The database connection.</param>
|
||||
/// <param name="backup">The backup to export.</param>
|
||||
/// <param name="includeSecrets">Whether to include secrets in the export.</param>
|
||||
/// <returns>The import/export structure.</returns>
|
||||
private static Serializable.ImportExportStructure PrepareBackupForExport(Connection databaseConnection, IBackup backup, bool includeSecrets)
|
||||
{
|
||||
var exported = databaseConnection.PrepareBackupForExport(backup);
|
||||
if (!includeSecrets && exported.Backup != null)
|
||||
{
|
||||
var clone = exported.Backup.Clone();
|
||||
clone.RemoveSensitiveInformation();
|
||||
exported = new Serializable.ImportExportStructure()
|
||||
{
|
||||
CreatedByVersion = exported.CreatedByVersion,
|
||||
Backup = clone,
|
||||
Schedule = exported.Schedule,
|
||||
DisplayNames = exported.DisplayNames
|
||||
};
|
||||
}
|
||||
return exported;
|
||||
}
|
||||
|
||||
public static IRunnerData CreateTask(DuplicatiOperation operation, IBackup backup, IDictionary<string, string?>? extraOptions = null, string[]? filterStrings = null, string[]? extraArguments = null, int pageSize = 0, int pageOffset = 0, bool returnExtended = false, bool caseSensitiveSearch = false)
|
||||
{
|
||||
return new RunnerData()
|
||||
@@ -670,7 +768,7 @@ namespace Duplicati.Server
|
||||
ApplyAdditionalTargetUrls(backup, options);
|
||||
|
||||
// Pack in the system or task config for easy restore
|
||||
if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config"))
|
||||
if (data.Operation == DuplicatiOperation.Backup)
|
||||
tempfolder = StoreTaskConfigAndGetTempFolder(databaseConnection, data, options);
|
||||
|
||||
var useOutOfProcess = databaseConnection.ApplicationSettings.UseOutOfProcessController;
|
||||
@@ -882,39 +980,47 @@ namespace Duplicati.Server
|
||||
if (data.Backup == null)
|
||||
throw new ArgumentNullException(nameof(data.Backup));
|
||||
|
||||
var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase);
|
||||
var this_task = Utility.ParseBool(options["store-task-config"], false);
|
||||
var mode = ParseStoreTaskConfigMode(options.GetValueOrDefault("store-task-config"));
|
||||
var encryptionEnabled = IsBackupEncryptionEnabled(options);
|
||||
var effectiveMode = ResolveEffectiveMode(mode, encryptionEnabled);
|
||||
|
||||
options.Remove("store-task-config");
|
||||
|
||||
TempFolder? tempfolder = null;
|
||||
if (all_tasks || this_task)
|
||||
if (effectiveMode == StoreTaskConfigMode.None)
|
||||
return null;
|
||||
|
||||
bool includeAllTasks = effectiveMode == StoreTaskConfigMode.All || effectiveMode == StoreTaskConfigMode.AllWithForcedSecrets;
|
||||
bool includeSecrets = effectiveMode == StoreTaskConfigMode.SelfWithForcedSecrets || effectiveMode == StoreTaskConfigMode.AllWithForcedSecrets;
|
||||
|
||||
var tempfolder = new TempFolder();
|
||||
var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
|
||||
using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath))
|
||||
{
|
||||
tempfolder = new TempFolder();
|
||||
var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
|
||||
using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath))
|
||||
IEnumerable<Serializable.ImportExportStructure>? taskdata = null;
|
||||
if (includeAllTasks)
|
||||
{
|
||||
object? taskdata = null;
|
||||
if (all_tasks)
|
||||
taskdata = databaseConnection.Backups.Where(x => !x.IsTemporary).Select(x => databaseConnection.PrepareBackupForExport(databaseConnection.GetBackup(x.ID)!));
|
||||
else
|
||||
taskdata = new[] { databaseConnection.PrepareBackupForExport(data.Backup) };
|
||||
|
||||
using (var fs = System.IO.File.OpenWrite(tempfile))
|
||||
using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
|
||||
Serializer.SerializeJson(sw, taskdata, true);
|
||||
|
||||
tempfile.Protected = true;
|
||||
|
||||
options.TryGetValue("control-files", out var controlfiles);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(controlfiles))
|
||||
controlfiles = tempfile;
|
||||
else
|
||||
controlfiles += System.IO.Path.PathSeparator + tempfile;
|
||||
|
||||
options["control-files"] = controlfiles;
|
||||
taskdata = databaseConnection.Backups
|
||||
.Where(x => !x.IsTemporary)
|
||||
.Select(x => PrepareBackupForExport(databaseConnection, databaseConnection.GetBackup(x.ID)!, includeSecrets));
|
||||
}
|
||||
else
|
||||
{
|
||||
taskdata = [PrepareBackupForExport(databaseConnection, data.Backup, includeSecrets)];
|
||||
}
|
||||
|
||||
using (var fs = System.IO.File.OpenWrite(tempfile))
|
||||
using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
|
||||
Serializer.SerializeJson(sw, taskdata, true);
|
||||
|
||||
options.TryGetValue("control-files", out var controlfiles);
|
||||
|
||||
// Append this to any other control files
|
||||
options["control-files"] = string.IsNullOrWhiteSpace(controlfiles)
|
||||
? controlfiles = tempfile
|
||||
: controlfiles += System.IO.Path.PathSeparator + tempfile;
|
||||
|
||||
// Don't delete the file now, leave it for when the folder is deleted
|
||||
tempfile.Protected = true;
|
||||
}
|
||||
return tempfolder;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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.
|
||||
|
||||
namespace Duplicati.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Defines how the task configuration should be stored in the backup.
|
||||
/// </summary>
|
||||
public enum StoreTaskConfigMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically determine behavior based on encryption settings.
|
||||
/// When encryption is enabled, behaves as <see cref="Self"/>.
|
||||
/// When encryption is not enabled, behaves as <see cref="None"/>.
|
||||
/// </summary>
|
||||
Auto,
|
||||
/// <summary>
|
||||
/// Include the current job's backup configuration.
|
||||
/// When encryption is enabled, includes all secrets.
|
||||
/// When encryption is not enabled, excludes secrets.
|
||||
/// </summary>
|
||||
Self,
|
||||
/// <summary>
|
||||
/// Include all job backup configurations.
|
||||
/// When encryption is enabled, includes all secrets.
|
||||
/// When encryption is not enabled, excludes secrets.
|
||||
/// </summary>
|
||||
All,
|
||||
/// <summary>
|
||||
/// Do not include any task configuration.
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// Include the current job's backup configuration with all secrets included.
|
||||
/// </summary>
|
||||
SelfWithForcedSecrets,
|
||||
/// <summary>
|
||||
/// Include all job backup configurations with all secrets included.
|
||||
/// </summary>
|
||||
AllWithForcedSecrets
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ Error message: {0}", error); }
|
||||
public static string ConfigureHttpsHostnamesLong { get { return LC.L(@"Comma-separated list of hostnames to include in the HTTPS certificate (used with --configure-https)"); } }
|
||||
public static string SuppressWelcomePageShort { get { return LC.L(@"Suppress the initial welcome page"); } }
|
||||
public static string SuppressWelcomePageLong { get { return LC.L(@"Suppress the welcome page that is shown when first using the web interface"); } }
|
||||
public static string StoretaskconfigShort { get { return LC.L(@"Store task configuration with backup"); } }
|
||||
public static string StoretaskconfigLong { get { return LC.L(@$"Controls whether the backup configuration is stored as a control file in the backup. When encryption is enabled, {StoreTaskConfigMode.Auto} stores the current job with secrets. When encryption is not enabled, {StoreTaskConfigMode.Auto} does not store anything. When encryption is not enabled {StoreTaskConfigMode.Self} and {StoreTaskConfigMode.All} store the configuration without secrets. Note that configuration is only uploaded if the backup has changed, and a configuration change alone will not be treated as a new backup version."); } }
|
||||
}
|
||||
internal static class Scheduler
|
||||
{
|
||||
|
||||
@@ -139,6 +139,11 @@ public sealed record SystemInfoDto
|
||||
/// </summary>
|
||||
public required IEnumerable<Library.Interface.ICommandLineArgument> Options { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server-only options.
|
||||
/// </summary>
|
||||
public required IEnumerable<Library.Interface.ICommandLineArgument> ServerOnlyOptions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression modules.
|
||||
/// </summary>
|
||||
|
||||
@@ -27,6 +27,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Server;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.WebserverCore.Abstractions;
|
||||
using Duplicati.WebserverCore.Dto.V2;
|
||||
@@ -103,6 +104,21 @@ public class DuplicatiWebserver
|
||||
private static readonly bool EnableSwagger = false;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets server-only options that are consumed by the server and not passed to the command-line client.
|
||||
/// </summary>
|
||||
public static ICommandLineArgument[] ServerOnlyOptions =>
|
||||
[
|
||||
new CommandLineArgument(
|
||||
"store-task-config",
|
||||
CommandLineArgument.ArgumentType.Enumeration,
|
||||
Server.Strings.Program.StoretaskconfigShort,
|
||||
Server.Strings.Program.StoretaskconfigLong,
|
||||
StoreTaskConfigMode.Auto.ToString(),
|
||||
null,
|
||||
Enum.GetNames(typeof(StoreTaskConfigMode)))
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The settings used for stating the server
|
||||
/// </summary>
|
||||
|
||||
@@ -175,6 +175,11 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
|
||||
/// </summary>
|
||||
public required Library.Interface.ICommandLineArgument[] Options { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server-only options.
|
||||
/// </summary>
|
||||
public required Library.Interface.ICommandLineArgument[] ServerOnlyOptions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression modules.
|
||||
/// </summary>
|
||||
@@ -283,6 +288,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
|
||||
NewLine = Environment.NewLine,
|
||||
CLRVersion = Environment.Version.ToString(),
|
||||
Options = Server.Serializable.ServerSettings.Options,
|
||||
ServerOnlyOptions = DuplicatiWebserver.ServerOnlyOptions,
|
||||
CompressionModules = Server.Serializable.ServerSettings.CompressionModules,
|
||||
EncryptionModules = Server.Serializable.ServerSettings.EncryptionModules,
|
||||
BackendModules = Server.Serializable.ServerSettings.BackendModules,
|
||||
@@ -372,6 +378,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
|
||||
NewLine = systeminfo.NewLine,
|
||||
CLRVersion = systeminfo.CLRVersion,
|
||||
Options = systeminfo.Options,
|
||||
ServerOnlyOptions = systeminfo.ServerOnlyOptions,
|
||||
CompressionModules = systeminfo.CompressionModules,
|
||||
EncryptionModules = systeminfo.EncryptionModules,
|
||||
BackendModules = systeminfo.BackendModules,
|
||||
|
||||
Reference in New Issue
Block a user