Fixed dry-run command

This PR adds fixes to honor the --dry-run option to avoid surprises when running an operation.

This fixes #6573
This closes #6581
This commit is contained in:
Kenneth Skovhede
2026-01-27 15:01:34 +01:00
parent 05d9457f36
commit a986a13abf
10 changed files with 252 additions and 17 deletions
+8 -1
View File
@@ -294,7 +294,14 @@ namespace Duplicati.Library.Main
{
try
{
await backendManager.DeleteAsync(list[i].File.Name, list[i].File.Size, true, result.TaskControl.ProgressToken).ConfigureAwait(false);
if (m_options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteFile", "Would delete file: {0}", list[i].File.Name);
}
else
{
await backendManager.DeleteAsync(list[i].File.Name, list[i].File.Size, true, result.TaskControl.ProgressToken).ConfigureAwait(false);
}
}
catch (Exception ex)
{
@@ -34,6 +34,8 @@ namespace Duplicati.Library.Main.Operation.Backup
/// </summary>
internal static class DataBlockProcessor
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(DataBlockProcessor));
public static Task Run(Channels channels, BackupDatabase database, IBackendManager backendManager, Options options, ITaskReader taskreader)
{
return AutomationExtensions.RunTask(
@@ -142,8 +144,15 @@ namespace Duplicati.Library.Main.Operation.Backup
blockvolume = null;
indexvolume = null;
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadFile", "Would upload file: {0}", blockVolumeCopy.RemoteFilename);
}
else
{
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
}
}
@@ -47,8 +47,16 @@ internal static class RecreateMissingIndexFiles
return;
await database.UpdateRemoteVolumeAsync(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, false, default, null, taskreader.ProgressToken).ConfigureAwait(false);
await database.CommitTransactionAsync("RecreateMissingIndexFile", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadFile", "Would upload file: {0}", w.RemoteFilename);
}
else
{
await database.CommitTransactionAsync("RecreateMissingIndexFile", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
}
@@ -172,8 +172,16 @@ namespace Duplicati.Library.Main.Operation.Backup
await database.AddIndexBlockLinkAsync(indexVolumeCopy.VolumeID, target.BlockVolume.VolumeID, taskreader.ProgressToken).ConfigureAwait(false);
}
await database.CommitTransactionAsync("UploadSpillVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(Logging.Log.LogTagFromType(typeof(SpillCollectorProcess)), "WouldUploadFile", "Would upload file: {0}", target.BlockVolume.RemoteFilename);
}
else
{
await database.CommitTransactionAsync("UploadSpillVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
}
@@ -67,9 +67,16 @@ internal static class UploadRealFilelist
await taskreader.ProgressRendevouz().ConfigureAwait(false);
await db.UpdateRemoteVolumeAsync(filesetvolume.RemoteFilename, RemoteVolumeState.Uploading, -1, null, false, default, null, taskreader.ProgressToken).ConfigureAwait(false);
await db.CommitTransactionAsync("CommitUpdateRemoteVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(filesetvolume, null, null, false, () => db.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadFile", "Would upload file: {0}", filesetvolume.RemoteFilename);
}
else
{
await db.CommitTransactionAsync("CommitUpdateRemoteVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(filesetvolume, null, null, false, () => db.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
else
@@ -116,8 +116,16 @@ namespace Duplicati.Library.Main.Operation.Backup
// Because it is registered as "Deleting", it will be removed from remote storage by the cleanup process if it exists
if (!string.IsNullOrWhiteSpace(lastTempFilelist.Name) && (lastTempFilelist.State == RemoteVolumeState.Uploading || lastTempFilelist.State == RemoteVolumeState.Temporary))
await database.UpdateRemoteVolumeAsync(lastTempFilelist.Name, RemoteVolumeState.Deleting, -1, null, false, default, null, taskreader.ProgressToken).ConfigureAwait(false);
await database.CommitTransactionAsync("CommitUpdateFilelistVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(fsw, null, null, true, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken);
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadFile", "Would upload file: {0}", fsw.RemoteFilename);
}
else
{
await database.CommitTransactionAsync("CommitUpdateFilelistVolume", true, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(fsw, null, null, true, () => database.FlushBackendMessagesAndCommitAsync(backendManager, taskreader.ProgressToken), taskreader.ProgressToken);
}
}
catch
{
@@ -377,7 +377,7 @@ namespace Duplicati.Library.Main.Operation
{
if (options.NoBackendverification)
{
await FilelistProcessor.VerifyLocalList(backendManager, database, result.TaskControl.ProgressToken).ConfigureAwait(false);
await FilelistProcessor.VerifyLocalList(backendManager, database, options.Dryrun, result.TaskControl.ProgressToken).ConfigureAwait(false);
await UpdateStorageStatsFromDatabase(result, database, options, backendManager, result.TaskControl.ProgressToken).ConfigureAwait(false);
}
else
@@ -389,7 +389,15 @@ namespace Duplicati.Library.Main.Operation
// The last temporary filelist was emptied, and scheduled for deletion, so we need to delete it
try
{
await backendManager.DeleteAsync(updatedLastTemp.Name, updatedLastTemp.Size, true, result.TaskControl.ProgressToken).ConfigureAwait(false);
if (options.Dryrun)
{
Log.WriteDryrunMessage(LOGTAG, "WouldDeleteFile", "Would delete file: {0}", updatedLastTemp.Name);
}
else
{
Log.WriteInformationMessage(LOGTAG, "DeletingFile", "Deleting file: {0}", updatedLastTemp.Name);
await backendManager.DeleteAsync(updatedLastTemp.Name, updatedLastTemp.Size, true, result.TaskControl.ProgressToken).ConfigureAwait(false);
}
}
catch (Exception ex)
{
@@ -67,9 +67,10 @@ namespace Duplicati.Library.Main.Operation
/// </summary>
/// <param name="backendManager"> The backend manager instance to use.</param>
/// <param name="database">The database to compare with.</param>
/// <param name="dryrun">If true, no changes will be made.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>A task that completes when the verification is done.</returns>
public static async Task VerifyLocalList(IBackendManager backendManager, LocalDatabase database, CancellationToken cancellationToken)
public static async Task VerifyLocalList(IBackendManager backendManager, LocalDatabase database, bool dryrun, CancellationToken cancellationToken)
{
var locallist = database.GetRemoteVolumes(cancellationToken);
await foreach (var i in locallist.ConfigureAwait(false))
@@ -84,10 +85,17 @@ namespace Duplicati.Library.Main.Operation
case RemoteVolumeState.Temporary:
case RemoteVolumeState.Deleting:
case RemoteVolumeState.Uploading:
Logging.Log.WriteInformationMessage(LOGTAG, "RemovingStaleFile", "Removing remote file listed as {0}: {1}", i.State, i.Name);
try
{
await backendManager.DeleteAsync(i.Name, i.Size, true, cancellationToken).ConfigureAwait(false);
if (dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldDeleteFile", "Would delete file: {0}", i.Name);
}
else
{
Logging.Log.WriteInformationMessage(LOGTAG, "RemovingStaleFile", "Removing remote file listed as {0}: {1}", i.State, i.Name);
await backendManager.DeleteAsync(i.Name, i.Size, true, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex)
{
@@ -98,7 +98,7 @@ namespace Duplicati.Library.Main.Operation
.ConfigureAwait(false);
if (m_options.NoBackendverification)
await FilelistProcessor.VerifyLocalList(backendManager, db, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
await FilelistProcessor.VerifyLocalList(backendManager, db, m_options.Dryrun, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
else
await FilelistProcessor.VerifyRemoteList(backendManager, m_options, db, m_result.BackendWriter, null, null, logErrors: true, verifyMode: FilelistProcessor.VerifyMode.VerifyStrict, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
}
+172
View File
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Main;
using NUnit.Framework;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
public class DryRunTests : BasicSetupHelper
{
[Test]
public void TestBackupDryRunDoesNotUpload()
{
var sourceFolder = Path.Combine(DATAFOLDER, "source");
var backendFolder = Path.Combine(TARGETFOLDER, "backend");
Directory.CreateDirectory(sourceFolder);
Directory.CreateDirectory(backendFolder);
// Create some test files
File.WriteAllText(Path.Combine(sourceFolder, "test.txt"), "test content");
var options = new Dictionary<string, string>(this.TestOptions)
{
["dry-run"] = "true"
};
using (var controller = new Controller($"file://{backendFolder}", options, null))
controller.Backup(new[] { sourceFolder });
// Verify no files were uploaded
var uploadedFiles = Directory.GetFiles(backendFolder);
Assert.AreEqual(0, uploadedFiles.Length, "No files should be uploaded during dry-run");
}
[Test]
public void TestDeleteAllRemoteFilesDryRunDoesNotDelete()
{
var sourceFolder = Path.Combine(DATAFOLDER, "source");
var backendFolder = Path.Combine(TARGETFOLDER, "backend");
Directory.CreateDirectory(sourceFolder);
Directory.CreateDirectory(backendFolder);
// Create some test files and run a backup
File.WriteAllText(Path.Combine(sourceFolder, "test.txt"), "test content");
using (var controller = new Controller($"file://{backendFolder}", this.TestOptions, null))
{
controller.Backup(new[] { sourceFolder });
}
// Count files before dry-run delete
var filesBefore = Directory.GetFiles(backendFolder).Length;
Assert.Greater(filesBefore, 0, "Should have files in backend after backup");
// Run delete-all-remote-files with dry-run
var options = new Dictionary<string, string>(this.TestOptions)
{
["dry-run"] = "true"
};
using (var controller = new Controller($"file://{backendFolder}", options, null))
{
controller.DeleteAllRemoteFiles();
}
// Verify files still exist
var filesAfter = Directory.GetFiles(backendFolder).Length;
Assert.AreEqual(filesBefore, filesAfter, "Files should not be deleted during dry-run");
}
[Test]
public void TestVerifyLocalListDryRunDoesNotDelete()
{
var sourceFolder = Path.Combine(DATAFOLDER, "source");
var backendFolder = Path.Combine(TARGETFOLDER, "backend");
Directory.CreateDirectory(sourceFolder);
Directory.CreateDirectory(backendFolder);
// Create some test files and run a backup
File.WriteAllText(Path.Combine(sourceFolder, "test.txt"), "test content");
using (var controller = new Controller($"file://{backendFolder}", this.TestOptions, null))
{
controller.Backup(new[] { sourceFolder });
}
var filesBefore = Directory.GetFiles(backendFolder).Length;
Assert.Greater(filesBefore, 0);
// Manually mark some files as "Uploading" in the database
using (var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={DBFILE}"))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = "UPDATE RemoteVolume SET State = \"Uploading\" WHERE Type = \"Files\"";
command.ExecuteNonQuery();
}
}
// Run a backup with dry-run (which triggers VerifyLocalList)
var options = new Dictionary<string, string>(this.TestOptions)
{
["dry-run"] = "true"
};
using (var controller = new Controller($"file://{backendFolder}", options, null))
{
controller.Backup(new[] { sourceFolder });
}
// Verify that the files were not deleted
var filesAfter = Directory.GetFiles(backendFolder).Length;
Assert.AreEqual(filesBefore, filesAfter, "Files should not be deleted during dry-run");
}
[Test]
public void TestInterruptedBackupRecoveryDryRunDoesNotUpload()
{
var sourceFolder = Path.Combine(DATAFOLDER, "source");
var backendFolder = Path.Combine(TARGETFOLDER, "backend");
Directory.CreateDirectory(sourceFolder);
Directory.CreateDirectory(backendFolder);
// Create some large files to ensure backup takes some time
var data = new byte[10 * 1024 * 1024]; // 10MB
new Random().NextBytes(data);
File.WriteAllBytes(Path.Combine(sourceFolder, "test.bin"), data);
// Start a backup and interrupt it
using (var controller = new Controller($"file://{backendFolder}", this.TestOptions, null))
{
var backupTask = Task.Run(() => controller.Backup(new[] { sourceFolder }));
Thread.Sleep(2000); // Wait 2 seconds
controller.Stop();
try
{
backupTask.Wait();
}
catch (AggregateException)
{
// Expected
}
}
// Now run recovery with dry-run
var options = new Dictionary<string, string>(this.TestOptions)
{
["dry-run"] = "true"
};
var filesBefore = Directory.GetFiles(backendFolder).Length;
using (var controller = new Controller($"file://{backendFolder}", options, null))
{
controller.Backup(new[] { sourceFolder });
}
// Verify no new files were uploaded
var filesAfter = Directory.GetFiles(backendFolder).Length;
Assert.AreEqual(filesBefore, filesAfter, "No files should be uploaded during dry-run recovery");
}
}
}