Files
Kenneth Skovhede fa0c9c72a9 Add guard against invalid entries
This PR guards agains inserting entries into the DeletedVolume if they reference non-existing volumes, which can happen in error scenarios.

This fixes #6553
2026-01-30 16:49:03 +01:00

110 lines
4.2 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 Issue6553 : BasicSetupHelper
{
[Test]
[Category("Issue6553")]
public void RecreateDbShouldNotLoseFilesetWhenDindexLacksDblock()
{
// Arrange
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["no-encryption"] = "true";
testopts["keep-versions"] = "0"; // Keep all versions
var sourceFolder = Path.Combine(DATAFOLDER, "source");
Directory.CreateDirectory(sourceFolder);
// Step 1: Create file A and backup (Backup 1)
File.WriteAllText(Path.Combine(sourceFolder, "A.txt"), "Content A");
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var res = c.Backup(new[] { sourceFolder });
Assert.AreEqual(0, res.Errors.Count());
}
// Save dindex1
var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*");
Assert.AreEqual(1, dindexFiles.Length);
var dindex1Path = Path.Combine(DATAFOLDER, "dindex1.zip");
File.Copy(dindexFiles[0], dindex1Path);
// Step 2: Delete DB and Backup A again (Backup 2)
// This forces A to be uploaded to a new dblock (dblock2)
File.Delete(testopts["dbpath"]);
foreach (var f in Directory.GetFiles(TARGETFOLDER))
File.Delete(f);
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var res = c.Backup(new[] { sourceFolder });
Assert.AreEqual(0, res.Errors.Count());
}
// Step 3: Prepare for Recreate
File.Delete(testopts["dbpath"]);
// Restore dindex1. Rename it to ensure it is processed FIRST.
var dindex1RestorePath = Path.Combine(TARGETFOLDER, "duplicati-00000000000000000000000000000000.dindex.zip");
File.Copy(dindex1Path, dindex1RestorePath);
// Step 4: Recreate the database
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
try
{
var res = c.Repair();
}
catch (Exception)
{
// Repair is expected to fail or warn due to missing blocks
}
}
// Step 5: Verify that we have at least one version (Backup 2)
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var listRes = c.List();
Assert.IsTrue(listRes.Filesets.Count() > 0,
"Expected at least one backup version after recreate, but found none");
}
// Step 6: Check for DeletedBlock entries pointing to Temporary volumes
using (var connection = new SqliteConnection($"Data Source={testopts["dbpath"]};Pooling=false"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"
SELECT COUNT(*)
FROM DeletedBlock
WHERE VolumeID IN (SELECT ID FROM RemoteVolume WHERE State = 'Temporary')
";
var tempCount = (long)command.ExecuteScalar();
// With the fix (and apparently even without it in this environment), this should be 0
Assert.AreEqual(0, tempCount, "Should not have DeletedBlock entries pointing to Temporary volumes");
command.CommandText = @"
SELECT COUNT(*)
FROM DeletedBlock
WHERE VolumeID NOT IN (SELECT ID FROM RemoteVolume)
";
var invalidCount = (long)command.ExecuteScalar();
Assert.AreEqual(0, invalidCount, "Should not have DeletedBlock entries with invalid VolumeID references");
}
}
}
}