// Copyright (C) 2025, 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. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Duplicati.Library.Interface; using Duplicati.Library.Main.Database; using Duplicati.Library.SQLiteHelper; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; namespace Duplicati.UnitTest { public class RepairWithMissingRemoteFiles : BasicSetupHelper { // TODO: Add more tests to cover the following scenarios: // Test where a single block in a large file is missing + run purge-broken-files // Test that fetching remote sources for blocks is working [Test] [Category("Targeted")] [TestCase(false, true, false)] [TestCase(true, true, false)] [TestCase(true, false, false)] public void TestRepairWithMissingDblocks(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true, number_of_retries = 0 }); // 1. Make a backup of a single file (1 block + 3 bytes for a block with less data) var data = new byte[1024 + 3]; data.AsSpan().Fill((byte)'a'); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Repair()); // 4. Test that the database is now valid using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Test()); } [Test] [Category("Targeted")] [TestCase(false, true, false)] [TestCase(true, true, false)] [TestCase(true, false, false)] public void TestRepairWithMissingDblocksAndMultipleSources(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data var data = new byte[1024 + 3]; data.AsSpan().Fill((byte)'a'); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data.AsSpan().Slice(0, 1024).ToArray()); File.WriteAllBytes(Path.Combine(DATAFOLDER, "c"), data.AsSpan().Slice(0, 1024).ToArray()); File.WriteAllBytes(Path.Combine(DATAFOLDER, "d"), data.AsSpan().Slice(1024, 3).ToArray()); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { TestUtils.AssertResults(c.Repair()); TestUtils.AssertResults(c.Test()); } } [Test] [Category("Targeted")] [TestCase(false, true, false)] [TestCase(true, true, false)] [TestCase(true, false, false)] public void TestRepairWithDataFillingMultipleBlocklists(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 5 bytes extra var data = new byte[1024 * 32 * 3 + 5]; data.AsSpan().Fill((byte)'a'); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data.AsSpan().Slice(1024, 3).ToArray()); Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "c"), data); Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "d"), data.AsSpan().Slice(0, 1028).ToArray()); File.WriteAllBytes(Path.Combine(DATAFOLDER, "e"), data.AsSpan().Slice(4098, 67).ToArray()); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { TestUtils.AssertResults(c.Repair()); TestUtils.AssertResults(c.Test()); } } [Test] [Category("Targeted")] [TestCase(true, true, true)] [TestCase(true, false, false)] public void TestRepairWithSmallFileOnly(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data var data = new byte[3]; data.AsSpan().Fill((byte)'a'); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { TestUtils.AssertResults(c.Repair()); TestUtils.AssertResults(c.Test()); } } [Test] [Category("Targeted")] [TestCase(false, true, false)] [TestCase(true, true, false)] [TestCase(true, false, false)] [TestCase(true, true, true)] [TestCase(true, false, true)] public void TestRepairWithDataFillingMultipleBlocklistsRandom(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { TestUtils.AssertResults(c.Repair()); TestUtils.AssertResults(c.Test()); } // 4. Verify that restore works using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER }), null)) TestUtils.AssertResults(c.Restore(null)); TestUtils.AssertDirectoryTreesAreEquivalent(DATAFOLDER, RESTOREFOLDER, !Library.Utility.Utility.ParseBoolOption(testopts, "skip-metadata"), "Restore"); } [Test] [Category("Targeted")] [TestCase(true, true, true)] [TestCase(true, false, false)] public void TestRepairWithDryRun(bool deleteDblockFiles, bool deleteIndexFiles, bool deleteDlistFiles) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some random data var data = new byte[1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files if (deleteDblockFiles) { var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); } if (deleteIndexFiles) { var dindexFiles = Directory.GetFiles(TARGETFOLDER, "*.dindex.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dindexFiles) File.Delete(file); } if (deleteDlistFiles) { var dlistFiles = Directory.GetFiles(TARGETFOLDER, "*.dlist.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dlistFiles) File.Delete(file); } (long remoteVolumes, long duplicatedBlocks, List blockVolumeIds, List<(long, long)> indexVolumeLinks) GetDbSignature() { using var db = SQLiteLoader.LoadConnection(DBFILE); using var cmd = db.CreateCommand(); var remoteVolumes = cmd.ExecuteScalarInt64("SELECT COUNT(*) FROM RemoteVolume"); var duplicatedBlocks = cmd.ExecuteScalarInt64("SELECT COUNT(*) FROM DuplicateBlock"); var blockVolumeIds = cmd.ExecuteReaderEnumerable("SELECT VolumeID FROM Block").Select(x => x.ConvertValueToInt64(0)).ToList(); var indexVolumeLinks = cmd.ExecuteReaderEnumerable("SELECT IndexVolumeID, BlockVolumeID FROM IndexBlockLink").Select(x => (x.ConvertValueToInt64(0), x.ConvertValueToInt64(1))).ToList(); return (remoteVolumes, duplicatedBlocks, blockVolumeIds, indexVolumeLinks); } var preSignature = GetDbSignature(); // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { dry_run = true }), null)) TestUtils.AssertResults(c.Repair()); var postSignature = GetDbSignature(); Assert.AreEqual(preSignature.remoteVolumes, postSignature.remoteVolumes, "Remote volumes count should be the same after dry run repair."); Assert.AreEqual(preSignature.duplicatedBlocks, postSignature.duplicatedBlocks, "Duplicated blocks count should be the same after dry run repair."); Assert.AreEqual(preSignature.blockVolumeIds.Count, postSignature.blockVolumeIds.Count, "Block volume IDs count should be the same after dry run repair."); Assert.AreEqual(preSignature.indexVolumeLinks.Count, postSignature.indexVolumeLinks.Count, "Index volume links count should be the same after dry run repair."); Assert.IsTrue(preSignature.blockVolumeIds.All(x => postSignature.blockVolumeIds.Contains(x)), "All block volume IDs should be present after dry run repair."); Assert.IsTrue(preSignature.indexVolumeLinks.All(x => postSignature.indexVolumeLinks.Contains(x)), "All index volume links should be present after dry run repair."); } [Test] [Category("Targeted")] public void TestRepairWithNoBlockAvailableFails() { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); // Destroy source data File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), new byte[0]); Directory.SetLastWriteTime(DATAFOLDER, DateTime.UtcNow.AddSeconds(1)); // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Repair(); Assert.IsTrue(res.Errors.Where(x => x.Contains("purge-broken-files")).Count() == 1, "Repair should fail with missing block data."); } } [Test] [Category("Targeted")] public void TestPartialRepairPossibleWithMissingMetadata() { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); // Destroy some source data Directory.SetLastWriteTime(DATAFOLDER, DateTime.UtcNow.AddSeconds(1)); // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Repair(); Assert.IsTrue(res.Warnings.Where(x => x.Contains("purge-broken-files")).Count() == 1, "Repair should warn about missing block data."); // Ensure that the partial repair did not clean up fully Assert.Throws(() => c.Test()); // Purge broken files should be possible TestUtils.AssertResults(c.PurgeBrokenFiles(null)); TestUtils.AssertResults(c.Test(int.MaxValue)); } // 4. Verify that restore works using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER }), null)) TestUtils.AssertResults(c.Restore(null)); TestUtils.AssertDirectoryTreesAreEquivalent(DATAFOLDER, RESTOREFOLDER, !Library.Utility.Utility.ParseBoolOption(testopts, "skip-metadata"), "Restore"); } [Test] [Category("Targeted")] [TestCase(false)] [TestCase(true)] public void TestPartialRepairPossibleWithDeletedFiles(bool deleteLargeFile) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data.AsSpan().Slice(0, 1024).ToArray()); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); // Destroy some source data (also destroys the metadata) if (deleteLargeFile) File.Delete(Path.Combine(DATAFOLDER, "a")); else File.Delete(Path.Combine(DATAFOLDER, "b")); // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Repair(); Assert.IsTrue(res.Warnings.Where(x => x.Contains("purge-broken-files")).Count() == 1, "Repair should warn about missing block data."); // Ensure that the partial repair did not clean up fully Assert.Throws(() => c.Test()); // Purge broken files should be possible TestUtils.AssertResults(c.PurgeBrokenFiles(null)); TestUtils.AssertResults(c.Test(int.MaxValue)); var files = c.List("*").Files.ToList(); Console.WriteLine("Files: " + string.Join(", ", files.Select(x => x.Path))); } // 4. Verify that restore works using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER }), null)) TestUtils.AssertResults(c.Restore(null)); Assert.That(File.Exists(Path.Combine(RESTOREFOLDER, "a")), Is.EqualTo(!deleteLargeFile), "Restored file 'a' should be restored."); Assert.That(File.Exists(Path.Combine(RESTOREFOLDER, "b")), Is.EqualTo(true), "File 'b' should always be restored."); if (!deleteLargeFile) Assert.That(File.ReadAllBytes(Path.Combine(RESTOREFOLDER, "a")), Is.EqualTo(data), "Restored file 'a' should be the same as the original."); else Assert.That(File.ReadAllBytes(Path.Combine(RESTOREFOLDER, "b")), Is.EqualTo(data.AsSpan().Slice(0, 1024).ToArray()), "Restored file 'b' should be the same as the original."); } [Test] [Category("Targeted")] [TestCase(0)] // Large file only [TestCase(1)] // Small file only [TestCase(2)] // Both files [TestCase(3)] // Metadata only public void TestPartialRepairPossibleWithPartialData(int scenario) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data.AsSpan().Slice(0, 1024).ToArray()); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToList(); foreach (var file in dblockFiles) File.Delete(file); var corruptLargeFile = scenario == 0 || scenario == 2; var corruptSmallFile = scenario == 1 || scenario == 2; var corruptBothFiles = corruptLargeFile && corruptSmallFile; var destroyMetadata = scenario == 3; // Destroy some source data (keep metadata) var prev = data[1]; data[1] = 1; if (corruptLargeFile) { var timestamp = File.GetLastWriteTime(Path.Combine(DATAFOLDER, "a")); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); File.SetLastWriteTime(Path.Combine(DATAFOLDER, "a"), timestamp); } if (corruptSmallFile) { var timestamp = File.GetLastWriteTime(Path.Combine(DATAFOLDER, "b")); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data.AsSpan().Slice(0, 1024).ToArray()); File.SetLastWriteTime(Path.Combine(DATAFOLDER, "b"), timestamp); } data[1] = prev; // Make sure that destroying the metadata does not destroy the files if (destroyMetadata) { File.SetLastWriteTime(Path.Combine(DATAFOLDER, "a"), DateTime.UtcNow.AddSeconds(1)); File.SetLastWriteTime(Path.Combine(DATAFOLDER, "b"), DateTime.UtcNow.AddSeconds(1.1)); } // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Repair(); if (corruptBothFiles || destroyMetadata) { Assert.IsTrue(res.Warnings.Where(x => x.Contains("purge-broken-files")).Count() == 1, "Repair should warn about missing block data."); // Ensure that the partial repair did not clean up fully Assert.Throws(() => c.Test()); } else { TestUtils.AssertResults(res); TestUtils.AssertResults(c.Test(int.MaxValue)); } // Purge broken files should be possible TestUtils.AssertResults(c.PurgeBrokenFiles(null)); TestUtils.AssertResults(c.Test(int.MaxValue)); } // 4. Verify that restore works using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER }), null)) { var res = c.Restore(null); if (corruptBothFiles) { // We get a warning because neither file can be restored Assert.That(res.Errors.Count(), Is.EqualTo(0), "Restore should not fail."); } else { TestUtils.AssertResults(res); } } Assert.That(File.Exists(Path.Combine(RESTOREFOLDER, "a")), Is.EqualTo(!corruptBothFiles), "File 'a' should be restored."); Assert.That(File.Exists(Path.Combine(RESTOREFOLDER, "b")), Is.EqualTo(!corruptBothFiles), "File 'b' should be restored."); if (!corruptLargeFile) Assert.That(Convert.ToBase64String(File.ReadAllBytes(Path.Combine(RESTOREFOLDER, "a"))), Is.EqualTo(Convert.ToBase64String(data)), "Restored file 'a' should be the same as the original."); else if (!corruptBothFiles) Assert.That(Convert.ToBase64String(File.ReadAllBytes(Path.Combine(RESTOREFOLDER, "b"))), Is.EqualTo(Convert.ToBase64String(data.AsSpan().Slice(0, 1024))), "Restored file 'b' should be the same as the original."); } [Test] [Category("Targeted")] [TestCase(1)] [TestCase(5)] [TestCase(10)] public void TestRepairPossibleWithMultipleDblockFiles(int dblocksToDelete) { var testopts = TestOptions.Expand(new { blocksize = "1kb", no_encryption = true, rebuild_missing_dblock_files = true, zip_compression_level = 0, dblock_size = "10kb" }); // 1. Make some shared data 1kb blocks, 32 bytes hash, 3 blocklists, and 1028 bytes extra var data = new byte[1024 * 32 * 3 + 1028]; Random.Shared.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) TestUtils.AssertResults(c.Backup([DATAFOLDER])); // 2. Delete files var dblockFiles = Directory.GetFiles(TARGETFOLDER, "*.dblock.*", SearchOption.TopDirectoryOnly).ToArray(); Random.Shared.Shuffle(dblockFiles); foreach (var file in dblockFiles.Take(dblocksToDelete)) File.Delete(file); // 3. Try to repair the remote destination using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { TestUtils.AssertResults(c.Repair()); TestUtils.AssertResults(c.Test()); } // 4. Verify that restore works using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER }), null)) TestUtils.AssertResults(c.Restore(null)); TestUtils.AssertDirectoryTreesAreEquivalent(DATAFOLDER, RESTOREFOLDER, !Library.Utility.Utility.ParseBoolOption(testopts, "skip-metadata"), "Restore"); } } }