diff --git a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs index 5247d9749..a4d5dfcd9 100644 --- a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs @@ -698,22 +698,20 @@ END "); public static IEnumerable GetFilesAndSourceBlocks(IDbConnection connection, string filetablename, string blocktablename, long blocksize, bool skipMetadata) { // TODO: Skip metadata as required - using (var cmd = connection.CreateCommand(FormatInvariant($@"SELECT DISTINCT ""A"".""TargetPath"", ""A"".""ID"", ""B"".""Hash"", (""B"".""Index"" * {blocksize}), ""B"".""Index"", ""B"".""Size"", ""C"".""Path"", (""D"".""Index"" * {blocksize}), ""E"".""Size"", ""B"".""Metadata"" FROM ""{filetablename}"" ""A"", ""{blocktablename}"" ""B"", ""File"" ""C"", ""BlocksetEntry"" ""D"", ""Block"" E WHERE ""A"".""ID"" = ""B"".""FileID"" AND ""C"".""BlocksetID"" = ""D"".""BlocksetID"" AND ""D"".""BlockID"" = ""E"".""ID"" AND ""B"".""Hash"" = ""E"".""Hash"" AND ""B"".""Size"" = ""E"".""Size"" AND ""B"".""Restored"" = 0"))) + // Have to order by target path and hash, to ensure BlockDescriptor and BlockSource match adjacent rows + using (var cmd = connection.CreateCommand(FormatInvariant($@"SELECT DISTINCT ""A"".""TargetPath"", ""A"".""ID"", ""B"".""Hash"", (""B"".""Index"" * {blocksize}), ""B"".""Index"", ""B"".""Size"", ""C"".""Path"", (""D"".""Index"" * {blocksize}), ""E"".""Size"", ""B"".""Metadata"" FROM ""{filetablename}"" ""A"", ""{blocktablename}"" ""B"", ""File"" ""C"", ""BlocksetEntry"" ""D"", ""Block"" E WHERE ""A"".""ID"" = ""B"".""FileID"" AND ""C"".""BlocksetID"" = ""D"".""BlocksetID"" AND ""D"".""BlockID"" = ""E"".""ID"" AND ""B"".""Hash"" = ""E"".""Hash"" AND ""B"".""Size"" = ""E"".""Size"" AND ""B"".""Restored"" = 0 ORDER BY ""A"".""TargetPath"", ""B"".""Index"""))) using (var rd = cmd.ExecuteReader()) { - if (rd.Read()) + var more = rd.Read(); + while (more) { - var more = true; - while (more) - { - var f = new LocalBlockSource(rd); - string current = f.TargetPath; - yield return f; + var f = new LocalBlockSource(rd); + string current = f.TargetPath; + yield return f; - more = f.HasMore; - while (more && current == f.TargetPath) - more = rd.Read(); - } + more = f.HasMore; + while (more && current == f.TargetPath) + more = rd.Read(); } } } diff --git a/Duplicati/UnitTest/RestoreHandlerTests.cs b/Duplicati/UnitTest/RestoreHandlerTests.cs index e381db79d..f3bb198ae 100644 --- a/Duplicati/UnitTest/RestoreHandlerTests.cs +++ b/Duplicati/UnitTest/RestoreHandlerTests.cs @@ -113,37 +113,31 @@ namespace Duplicati.UnitTest { return; } - /* TODO-DNC - string folderPath = Path.Combine(this.DATAFOLDER, "folder"); + + var folderPath = Path.Combine(this.DATAFOLDER, "folder"); Directory.CreateDirectory(folderPath); - string filePath = Path.Combine(folderPath, "file"); - File.WriteAllBytes(filePath, new byte[] {0}); + var filePath = Path.Combine(folderPath, "file"); + File.WriteAllBytes(filePath, new byte[] { 0 }); // Protect access rules on the file. - FileSecurity fileSecurity = new FileInfo(filePath).GetAccessControl(); + var fileSecurity = new FileInfo(filePath).GetAccessControl(); fileSecurity.SetAccessRuleProtection(true, true); new FileInfo(filePath).SetAccessControl(fileSecurity); - Dictionary options = new Dictionary(this.TestOptions); - using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) - { - IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); - Assert.AreEqual(0, backupResults.Errors.Count()); - Assert.AreEqual(0, backupResults.Warnings.Count()); - } + var options = new Dictionary(this.TestOptions); + using (var c = new Controller("file://" + this.TARGETFOLDER, options, null)) + TestUtils.AssertResults(c.Backup(new[] { this.DATAFOLDER })); // First, restore without restoring permissions. - Dictionary restoreOptions = new Dictionary(this.TestOptions) {["restore-path"] = this.RESTOREFOLDER}; - using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + var restoreOptions = new Dictionary(this.TestOptions) { ["restore-path"] = this.RESTOREFOLDER }; + using (var c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) { - IRestoreResults restoreResults = c.Restore(new[] {filePath}); - Assert.AreEqual(0, restoreResults.Errors.Count()); - Assert.AreEqual(0, restoreResults.Warnings.Count()); + TestUtils.AssertResults(c.Restore(new[] { filePath })); - string restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); + var restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); Assert.IsTrue(File.Exists(restoredFilePath)); - FileSecurity restoredFileSecurity = new FileInfo(restoredFilePath).GetAccessControl(); + var restoredFileSecurity = new FileInfo(restoredFilePath).GetAccessControl(); Assert.IsFalse(restoredFileSecurity.AreAccessRulesProtected); // Remove the restored file so that the later restore avoids the "Restore completed @@ -154,19 +148,46 @@ namespace Duplicati.UnitTest // Restore with restoring permissions. restoreOptions["overwrite"] = "true"; restoreOptions["restore-permissions"] = "true"; - using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + using (var c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) { - IRestoreResults restoreResults = c.Restore(new[] {filePath}); - Assert.AreEqual(0, restoreResults.Errors.Count()); - Assert.AreEqual(0, restoreResults.Warnings.Count()); + TestUtils.AssertResults(c.Restore(new[] { filePath })); - string restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); + var restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); Assert.IsTrue(File.Exists(restoredFilePath)); - FileSecurity restoredFileSecurity = new FileInfo(restoredFilePath).GetAccessControl(); + var restoredFileSecurity = new FileInfo(restoredFilePath).GetAccessControl(); Assert.IsTrue(restoredFileSecurity.AreAccessRulesProtected); } - */ + } + + [Test] + [Category("RestoreHandler")] + public void RestoreWithoutLocalData([Values("true", "false")] string noLocalDb, [Values("true", "false")] string patchWithLocalBlocks) + { + var file1Path = Path.Combine(this.DATAFOLDER, "file1"); + File.WriteAllBytes(file1Path, new byte[] { 1, 2, 3 }); + + var file2Path = Path.Combine(this.DATAFOLDER, "file2"); + File.WriteAllBytes(file2Path, new byte[] { 3, 4, 6 }); + + var folderPath = Path.Combine(this.DATAFOLDER, "folder"); + Directory.CreateDirectory(folderPath); + systemIO.FileCopy(file1Path, Path.Combine(folderPath, "file1 copy"), true); + + using (Controller c = new Controller("file://" + this.TARGETFOLDER, new Dictionary(this.TestOptions), null)) + TestUtils.AssertResults(c.Backup(new[] { this.DATAFOLDER })); + + var restoreOptions = new Dictionary(this.TestOptions) + { + ["restore-path"] = this.RESTOREFOLDER, + ["no-local-db"] = noLocalDb, + ["restore-with-local-blocks"] = patchWithLocalBlocks + }; + + using (var c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + TestUtils.AssertResults(c.Restore(new[] { "*" })); + + TestUtils.AssertDirectoryTreesAreEquivalent(this.DATAFOLDER, this.RESTOREFOLDER, true, "Restoring without local data"); } } } \ No newline at end of file