Files
duplicati/Duplicati/UnitTest/Issue6552.cs
T
Kenneth Skovhede fa6be206ef Handle missing blocks on recreate
This PR handles a case where the `dindex` files may reference non-existing `dblock` files. In this case the recreate may register missing (and unused) blocks in the `DeletedBlock` table, which will later cause issues if the blocks are attempted used.

The fix is to remove missing blocks before the cleanup so there is no entry that references the missing volumes.

This fixes issue #6552
2026-01-28 21:03:03 +01:00

113 lines
4.3 KiB
C#

using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using Duplicati.Library.Main;
using Microsoft.Data.Sqlite;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
public class Issue6552 : BasicSetupHelper
{
[Test]
[Category("Issue6552")]
public void RecreateDbShouldNotCreateInvalidDeletedBlockReferences()
{
// Arrange
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["no-encryption"] = "true"; // Easier to manipulate files
var sourceFolder = Path.Combine(DATAFOLDER, "source");
Directory.CreateDirectory(sourceFolder);
// Step 1: Create file A.txt and backup
File.WriteAllText(Path.Combine(sourceFolder, "A.txt"), "Content A");
string savedDindexPath = null;
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var res = c.Backup(new[] { sourceFolder });
Assert.AreEqual(0, res.Errors.Count());
}
// Save the dindex file from first backup
var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*");
Assert.IsTrue(dindexFiles.Length > 0, "Should have at least one dindex file");
savedDindexPath = Path.Combine(DATAFOLDER, Path.GetFileName(dindexFiles[0]));
File.Copy(dindexFiles[0], savedDindexPath);
// Identify the dblock and dlist from the first backup
var firstDblock = Directory.GetFiles(TARGETFOLDER, "*.dblock.*").First();
var firstDlist = Directory.GetFiles(TARGETFOLDER, "*.dlist.*").First();
// Step 2: Delete A.txt, create B.txt and backup
File.Delete(Path.Combine(sourceFolder, "A.txt"));
File.WriteAllText(Path.Combine(sourceFolder, "B.txt"), "Content B - different content");
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var res = c.Backup(new[] { sourceFolder });
Assert.AreEqual(0, res.Errors.Count());
}
// Step 3: Delete the dblock file that the saved dindex references
// and add the old dindex back
// Delete the dblock associated with the first backup
if (File.Exists(firstDblock))
{
File.Delete(firstDblock);
}
// Delete the dlist associated with the first backup to ensure blocks are orphaned
if (File.Exists(firstDlist))
{
File.Delete(firstDlist);
}
// Delete the database to force recreate
File.Delete(testopts["dbpath"]);
// Add the old dindex file back (which references a dblock that may not exist)
File.Copy(savedDindexPath, Path.Combine(TARGETFOLDER, Path.GetFileName(savedDindexPath)), true);
// Step 4: Recreate the database
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
// This should complete but may create invalid references
try
{
var res = c.Repair();
}
catch (Exception ex)
{
// Recreate might fail, but we want to check the DB state
TestContext.Progress.WriteLine($"Repair failed: {ex.Message}");
}
}
// Step 5: Check for invalid DeletedBlock references
using (var connection = new SqliteConnection($"Data Source={testopts["dbpath"]};Pooling=false"))
{
connection.Open();
var command = connection.CreateCommand();
// Check for DeletedBlock entries with invalid VolumeID
command.CommandText = @"
SELECT COUNT(*)
FROM DeletedBlock
WHERE VolumeID NOT IN (SELECT ID FROM RemoteVolume)
";
var invalidCount = (long)command.ExecuteScalar();
// This assertion will fail with the current bug
Assert.AreEqual(0, invalidCount,
$"Found {invalidCount} DeletedBlock entries with invalid VolumeID references");
}
}
}
}