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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user