Files
duplicati/Duplicati/UnitTest/IssueTests.cs
T
Kenneth Skovhede 179d1ceb47 Make controller Async
This PR has a large blast radius because it takes the final step and bumps up the Controller to be fully async.

We have historically done a piece-by-piece update, so all operations were already async but the controller interface was kept synchronous.

With this update, the controller is now fully async and all tests are updated.

Most places where the new C# compiler warns about function names not ending in `Async` were also adressed, giving a massive refactor change.

Functionally, no changes are done.
2026-05-13 15:04:13 +02:00

938 lines
46 KiB
C#

// Copyright (C) 2026, 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 Duplicati.Library.DynamicLoader;
using Duplicati.Library.Interface;
using Duplicati.Library.Main;
using Microsoft.Data.Sqlite;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
public class IssueTests : BasicSetupHelper
{
[Test]
[Category("Targeted")]
public async Task Issue5023ReferencedFileMissingAsync([Values] bool compactBeforeRecreate)
{
// Reproduction for part of issue #5023
// Error during repair: "Remote file referenced as x by y, but not found in list, registering a missing remote file"
// Can be caused by interrupted index upload followed by compact before the repair
var testopts = new Dictionary<string, string>(TestOptions)
{
["number-of-retries"] = "0",
["dblock-size"] = "20KB",
["threshold"] = "1",
["keep-versions"] = "1",
["no-auto-compact"] = "true",
["no-encryption"] = "true"
};
const long filesize = 1024;
// First backup OK to create valid database
string target = "file://" + TARGETFOLDER;
string file1 = Path.Combine(DATAFOLDER, "f1");
TestUtils.WriteTestFile(file1, filesize);
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
// Sleep to ensure timestamps are different
Thread.Sleep(1000);
// Fail after index file put
target = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
string interruptedName = "";
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
// Fail dindex upload
if (action == DeterministicErrorBackend.BackendAction.PutAfter && remotename.Contains("dindex"))
{
interruptedName = remotename;
return true;
}
return false;
};
BackendLoader.AddBackend(new DeterministicErrorBackend());
string file2 = Path.Combine(DATAFOLDER, "f2");
TestUtils.WriteTestFile(file2, filesize);
var uploadEx = Assert.CatchAsync(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
await c.BackupAsync(new[] { DATAFOLDER });
});
while (uploadEx is AggregateException && uploadEx.InnerException is not null)
uploadEx = uploadEx.InnerException;
Assert.That(uploadEx, Is.TypeOf<DeterministicErrorBackend.DeterministicErrorBackendException>());
Console.WriteLine("Interrupted after upload of {0}", interruptedName);
// Sleep to ensure timestamps are different
Thread.Sleep(1000);
// Complete upload
target = "file://" + TARGETFOLDER;
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
// At this point there are two index files for the last dblock
Console.WriteLine("Target folder contents (expect extra index file):");
Console.WriteLine(string.Join("\n", from v in Directory.EnumerateFiles(TARGETFOLDER) select Path.GetFileName(v)));
// Note: If there is a recreate here (between the above extra file creation and the following compact),
// the bug does not occur as the extra index file will be recorded in the database and properly deleted by compact
if (compactBeforeRecreate)
{
// Make room for a backup
Thread.Sleep(2000);
// Delete f2 and compact
File.Delete(file2);
using (var c = new Controller(target, testopts, null))
{
var backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
ICompactResults compactResults = await c.CompactAsync();
TestUtils.AssertResults(compactResults);
Assert.Greater(compactResults.DeletedFileCount, 0);
}
// At this point there are two index files for the last dblock
Console.WriteLine("Target folder contents (expect extra index file):");
Console.WriteLine(string.Join("\n", from v in Directory.EnumerateFiles(TARGETFOLDER) select Path.GetFileName(v)));
}
// Database recreate should work (fails after compact)
File.Delete(DBFILE);
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.RepairAsync());
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue5825RestoreNoOverwriteAsync([Values] bool legacy, [Values] bool local_blocks)
{
// Reproduction of Issue #5825
// The logic in the previous version was to create a timestamped version of the file being restored to, if it already exists.
// It appears the new restore flow is not correctly doing the same.
// See forum thread: https://forum.duplicati.com/t/save-different-versions-with-timestamp-in-file-name-broken/19805
var testopts = new Dictionary<string, string>(TestOptions)
{
["restore-legacy"] = legacy.ToString().ToLower(),
["restore-with-local-blocks"] = local_blocks.ToString().ToLower()
};
int test_filesize = 1024;
// TODO check if the original files are untocuched
var original_dir = Path.Combine(DATAFOLDER, "some_original_dir");
Directory.CreateDirectory(original_dir);
string f0 = Path.Combine(original_dir, "some_file");
TestUtils.WriteTestFile(f0, test_filesize);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Attempt to restore the file
testopts["restore-path"] = RESTOREFOLDER;
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored");
}
// Verify that the files are equal
string f1 = Path.Combine(RESTOREFOLDER, "some_file");
Assert.That(File.ReadAllBytes(f0), Is.EqualTo(File.ReadAllBytes(f1)), "Restored file should be equal to original file");
// Modify the restored file
TestUtils.WriteTestFile(f1, test_filesize);
// Restore the file again, with overwrite.
testopts["overwrite"] = "true";
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored");
}
// Verify that the files are equal
Assert.That(File.ReadAllBytes(f0), Is.EqualTo(File.ReadAllBytes(f1)), "Restored file should be equal to original file");
// Save the timestamp of the file
var timestamp = File.GetLastWriteTime(f1);
// Touch the file
File.SetLastWriteTime(f1, DateTime.Now);
// Check that the timestamp has changed, but the file is still equal
Assert.That(File.GetLastWriteTime(f1), Is.Not.EqualTo(timestamp), "Timestamp should have changed");
Assert.That(File.ReadAllBytes(f0), Is.EqualTo(File.ReadAllBytes(f1)), "Restored file should be equal to original file");
// Restore the file again, with overwrite, should restore the timestamp of the file
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(0), "File should not have been restored, only the metadata.");
}
// Verify that the files are equal and the timestamp is restored
Assert.That(File.ReadAllBytes(f0), Is.EqualTo(File.ReadAllBytes(f1)), "Restored file should be equal to original file");
Assert.That(File.GetLastWriteTime(f1), Is.EqualTo(timestamp), "Timestamp should be restored");
// Modify the restored file
TestUtils.WriteTestFile(f1, test_filesize);
var f1_original = File.ReadAllBytes(f1);
// Restore the file again, without overwrite.
testopts["overwrite"] = "false";
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored");
}
// Verify that f1 is still the same
Assert.That(File.ReadAllBytes(f1), Is.EqualTo(f1_original), "The first restored file should remain untouched");
// Verify that there exists a new file with a timestamp
var files = Directory.GetFiles(RESTOREFOLDER, "*", SearchOption.TopDirectoryOnly);
Assert.That(files.Length, Is.EqualTo(2), "There should be two files in the folder");
var f2 = files.FirstOrDefault(v => v != f1);
// Modify the new restored file as well
TestUtils.WriteTestFile(f2, test_filesize);
var f2_original = File.ReadAllBytes(f2);
// Restore the file again, without overwrite.
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored");
}
// Verify that f1 and f2 are still the same
Assert.That(File.ReadAllBytes(f1), Is.EqualTo(f1_original), "The first restored file should remain untouched");
Assert.That(File.ReadAllBytes(f2), Is.EqualTo(f2_original), "The second restored file should remain untouched");
// Verify that there exists a new file with a timestamp
files = Directory.GetFiles(RESTOREFOLDER, "*", SearchOption.TopDirectoryOnly);
Assert.That(files.Length, Is.EqualTo(3), "There should be three files in the folder");
// Verify that the files are equal
var f3 = files.FirstOrDefault(v => v != f1 && v != f2);
Assert.That(File.ReadAllBytes(f0), Is.EqualTo(File.ReadAllBytes(f3)), "Restored file should be equal to original file");
// Modify the second file to match the original file - should not restore any files
File.WriteAllBytes(f2, File.ReadAllBytes(f0));
// Restore the file again, without overwrite.
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f0]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(0), "File should not have been restored");
}
// Verify that f1 is still untouched
Assert.That(File.ReadAllBytes(f1), Is.EqualTo(f1_original), "The first restored file should remain untouched");
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue5826RestoreMissingFolderAsync([Values] bool compressRestorePaths, [Values] bool restoreLegacy)
{
// Reproduction of Issue #5826
// With the new restore engine, it looks like a file will not be restored if the folder is not selected for restore.
// See forum issue for details: https://forum.duplicati.com/t/could-not-find-a-part-of-the-path/19775
var testopts = new Dictionary<string, string>(TestOptions)
{
["dont-compress-restore-paths"] = (!compressRestorePaths).ToString().ToLower(),
["restore-legacy"] = restoreLegacy.ToString().ToLower()
};
var original_dir = Path.Combine(DATAFOLDER, "some_original_dir");
Directory.CreateDirectory(original_dir);
string f = Path.Combine(original_dir, "some_file");
TestUtils.WriteTestFile(f, 1024 * 20);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Attempt to restore a file without selecting its parent folder
testopts["restore-path"] = RESTOREFOLDER;
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored.");
Assert.That(restoreResults.Warnings.Count(), Is.EqualTo(compressRestorePaths ? 0 : 1), "Warning should be generated for missing folder");
}
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue5886RestoreModifiedMiddleBlockAsync()
{
var blocksize = 1024;
var n_blocks = 5;
var testopts = new Dictionary<string, string>(TestOptions)
{
["blocksize"] = $"{blocksize}b",
["overwrite"] = "true"
};
var original_dir = Path.Combine(DATAFOLDER, "some_original_dir");
Directory.CreateDirectory(original_dir);
string f = Path.Combine(original_dir, "some_file");
TestUtils.WriteTestFile(f, n_blocks * blocksize);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
var original_contents = File.ReadAllBytes(f);
var new_block = new byte[blocksize];
new Random(5888).NextBytes(new_block);
var new_contents = new byte[n_blocks * blocksize];
for (int i = 0; i < n_blocks; i++)
{
for (int j = i; j < n_blocks; j++)
{
// Modify the blocks
for (int k = 0; k < n_blocks; k++)
{
if (k == i || k == j)
{
Array.Copy(new_block, 0, new_contents, k * blocksize, blocksize);
}
else
{
Array.Copy(original_contents, k * blocksize, new_contents, k * blocksize, blocksize);
}
}
// Write the modified file
File.WriteAllBytes(f, new_contents);
var restored_contents = File.ReadAllBytes(f);
Assert.That(restored_contents, Is.Not.EqualTo(original_contents), "Restored file should not be equal to original file");
// Attempt to restore the file
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored.");
}
// Verify that the files are equal
restored_contents = File.ReadAllBytes(f);
Assert.That(restored_contents, Is.EqualTo(original_contents), "Restored file should be equal to original file");
}
}
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue5957RestoreModifiedBlockUseLocalBlocksAsync([Values] bool use_local, [Values] bool overwrite)
{
var blocksize = 1024;
var testopts = new Dictionary<string, string>(TestOptions)
{
["blocksize"] = $"{blocksize}b",
["restore-with-local-blocks"] = use_local.ToString().ToLower(),
["overwrite"] = overwrite.ToString().ToLower()
};
var original_dir = Path.Combine(DATAFOLDER, "some_original_dir");
Directory.CreateDirectory(original_dir);
string f = Path.Combine(original_dir, "some_file");
TestUtils.WriteTestFile(f, 3 * blocksize);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
var original_contents = File.ReadAllBytes(f);
using (var stream = File.Open(f, FileMode.Open, FileAccess.ReadWrite))
{
stream.Seek(0, SeekOrigin.Begin);
var buffer = new byte[1];
if (await stream.ReadAsync(buffer, 0, 1) < 1)
throw new InvalidOperationException("Failed to read from test file");
buffer[0] = (byte)~buffer[0];
stream.Seek(0, SeekOrigin.Begin);
await stream.WriteAsync(buffer, 0, 1);
}
var restored_contents = File.ReadAllBytes(f);
Assert.That(restored_contents, Is.Not.EqualTo(original_contents), "Restored file should not be equal to original file");
// Attempt to restore the file
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([f]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(1), "File should have been restored.");
}
// Verify that the files are equal
// List files in folder
if (overwrite)
{
restored_contents = File.ReadAllBytes(f);
}
else
{
var files = Directory.GetFiles(original_dir, "*", SearchOption.TopDirectoryOnly);
Assert.That(files.Length, Is.EqualTo(2), "There should be two files in the folder");
var f2 = files.FirstOrDefault(v => v != f);
restored_contents = File.ReadAllBytes(f2);
}
Assert.That(restored_contents, Is.EqualTo(original_contents), "Restored file should be equal to original file");
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue6068FolderMetadataAsync([Values] bool restoreLegacy)
{
// Reproduction of Issue #6068
// The folder metadata is not restored
var testopts = new Dictionary<string, string>(TestOptions);
var original_dir_name = "some_original_dir";
var original_dir = Path.Combine(DATAFOLDER, original_dir_name);
var restored_dir = Path.Combine(RESTOREFOLDER, original_dir_name);
Directory.CreateDirectory(original_dir);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Sleep to ensure timestamps are different
await Task.Delay(1000);
// Attempt to restore to another folder
testopts["restore-path"] = RESTOREFOLDER;
testopts["restore-legacy"] = restoreLegacy.ToString().ToLower();
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
var restoreResults = await c.RestoreAsync([]);
Assert.That(restoreResults.RestoredFiles, Is.EqualTo(0), "No files should have been restored.");
}
// Assert that the original folders date modified and date created are the same as the restored folder
var original_dir_info = new DirectoryInfo(original_dir);
var restored_dir_info = new DirectoryInfo(restored_dir);
Assert.That(original_dir_info.CreationTime, Is.EqualTo(restored_dir_info.CreationTime), "Creation time should be equal");
Assert.That(original_dir_info.LastWriteTime, Is.EqualTo(restored_dir_info.LastWriteTime), "Last write time should be equal");
}
[Test]
[Category("Test"), Category("Bug")]
public async Task Issue6459HashNullInTestAsync()
{
// Reproduction of the issue outlined in https://forum.duplicati.com/t/value-cannot-be-null-parameter-hash-cannot-be-null/20958
// This test is to ensure that a null hash in the database does not cause issues during backup and repair operations.
var testopts = new Dictionary<string, string>(TestOptions)
{
["full-remote-verification"] = "true"
};
testopts.Remove("backup-test-samples");
// Create some test files
Directory.CreateDirectory(DATAFOLDER);
string f = Path.Combine(DATAFOLDER, "some_file");
TestUtils.WriteTestFile($"{f}_0.txt", 1024);
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Set one of the hashes in the database to null
{
using var con = new SqliteConnection($"Data source={DBFILE};Pooling=false");
await con.OpenAsync();
using var cmd = con.CreateCommand();
using var transaction = con.BeginTransaction();
cmd.Transaction = transaction;
cmd.CommandText = @"UPDATE Remotevolume SET Hash = NULL WHERE Name LIKE '%.dblock.%'";
await cmd.ExecuteNonQueryAsync();
await transaction.CommitAsync();
}
// Add another file and perform the backup again.
// This would fail prior to the fix.
TestUtils.WriteTestFile($"{f}_1.txt", 1024);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Delete and recreate the database to ensure the null hash is not present anymore
File.Delete(DBFILE);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.RepairAsync());
// Double check that the null hash is not present anymore
{
using var con = new SqliteConnection($"Data source={DBFILE};Pooling=false");
await con.OpenAsync();
using var cmd = con.CreateCommand();
cmd.CommandText = @"SELECT COUNT(*) FROM Remotevolume WHERE Hash IS NULL";
var count = (long)await cmd.ExecuteScalarAsync();
Assert.That(count, Is.EqualTo(0), "There should be no null hashes in the database.");
}
// Perform a final test backup to ensure everything is working correctly
TestUtils.WriteTestFile($"{f}_2.txt", 1024);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
{
IBackupResults backupResults = await c.BackupAsync([DATAFOLDER]);
TestUtils.AssertResults(backupResults);
}
}
[Test]
[Category("Restore"), Category("Bug")]
public async Task Issue6068FileAndFolderAttributesAndPermissionsAsync([Values] bool restorePermissions, [Values] bool restoreLegacy, [Values] bool skip_metadata)
{
// This test is to verify that permissions are restored correctly
// if the restore-permissions option is set, and that the
// attributes are restored regardless. Some discussion is in the
// related PR #6079
// Set the options according to the test parameters
var testopts = new Dictionary<string, string>(TestOptions)
{
["restore-permissions"] = restorePermissions.ToString().ToLower(),
["restore-legacy"] = restoreLegacy.ToString().ToLower(),
["skip-metadata"] = skip_metadata.ToString().ToLower()
};
// The attributes to test
var attrs = new FileAttributes[] {
FileAttributes.ReadOnly,
FileAttributes.Hidden,
FileAttributes.System,
// TODO The Archive attribute has been disabled for now, as the CI doesn't behave as expected, but it works locally.
//FileAttributes.Archive
};
// Create the generated directories and files
var original_dir = Path.Combine(DATAFOLDER, "some_original_dir");
var dirs = attrs
.Select(x => Path.Combine(original_dir, $"dir_{x}"))
.Prepend(original_dir)
.ToArray();
foreach (var dir in dirs)
Directory.CreateDirectory(dir);
foreach (var attr in attrs)
foreach (var dir in dirs)
{
var file = Path.Combine(dir, $"file_{attr}");
TestUtils.WriteTestFile(file, 1024);
File.SetAttributes(file, attr);
}
foreach (var (attr, dir) in attrs.Zip(dirs.Skip(1)))
_ = new DirectoryInfo(dir)
{
Attributes = attr
};
// Create OS specific files and directories
var os_special_dir = Path.Combine(original_dir, "os_special_dir");
Directory.CreateDirectory(os_special_dir);
var default_dir_attrs = new DirectoryInfo(os_special_dir).Attributes;
var os_special_file = Path.Combine(os_special_dir, "os_special_file");
TestUtils.WriteTestFile(os_special_file, 1024);
var default_file_attrs = File.GetAttributes(os_special_file);
if (OperatingSystem.IsWindows())
{
// Set only the current user to have access to both the file and the directory
var dir_info = new DirectoryInfo(os_special_dir);
var rules_dir = dir_info.GetAccessControl();
rules_dir.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
rules_dir.AddAccessRule(new FileSystemAccessRule(Environment.UserName, FileSystemRights.FullControl, AccessControlType.Allow));
rules_dir.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), FileSystemRights.ReadAndExecute, AccessControlType.Allow));
dir_info.SetAccessControl(rules_dir);
var file_info = new FileInfo(os_special_file);
var rules_file = file_info.GetAccessControl();
rules_file.AddAccessRule(new FileSystemAccessRule(Environment.UserName, FileSystemRights.FullControl, AccessControlType.Allow));
rules_file.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), FileSystemRights.ReadAndExecute, AccessControlType.Allow));
file_info.SetAccessControl(rules_file);
}
else // Mac and Linux
{
// Set the dir to 700 and the file to 600
_ = new DirectoryInfo(os_special_dir)
{
UnixFileMode = UnixFileMode.UserExecute | UnixFileMode.UserRead | UnixFileMode.UserWrite
};
_ = new FileInfo(os_special_file)
{
UnixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite
};
}
try
{
// Backup the files
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Restore the files
testopts["restore-path"] = RESTOREFOLDER;
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null))
await c.RestoreAsync([]);
// Check the folder attributes
foreach (var dir in dirs.Skip(1))
{
var restored_dir = dir.Replace(DATAFOLDER, RESTOREFOLDER);
var original_attrs = new DirectoryInfo(dir).Attributes;
var restored_attrs = new DirectoryInfo(restored_dir).Attributes;
if (skip_metadata && original_attrs != default_dir_attrs)
Assert.That(original_attrs, Is.Not.EqualTo(restored_attrs), $"Directory attributes should not be equal for directory: {dir}");
else
Assert.That(original_attrs, Is.EqualTo(restored_attrs), $"Directory attributes should be equal for directory: {dir}");
}
// Check the file attributes
foreach (var attr in attrs)
foreach (var dir in dirs)
{
var filename = $"file_{attr}";
var original_file = Path.Combine(dir, filename);
var restored_file = original_file.Replace(DATAFOLDER, RESTOREFOLDER);
// TODO we ignore the Archive attribute, as the CI sets this sporadically
var original_attrs = File.GetAttributes(original_file);
var restored_attrs = File.GetAttributes(restored_file);
if (skip_metadata && original_attrs != default_file_attrs)
Assert.That(original_attrs & ~FileAttributes.Archive, Is.Not.EqualTo(restored_attrs & ~FileAttributes.Archive), $"File attributes should not be equal for original file: {original_file}");
else
Assert.That(original_attrs & ~FileAttributes.Archive, Is.EqualTo(restored_attrs & ~FileAttributes.Archive), $"File attributes should be equal for original file: {original_file}");
}
// Check the OS specific file and directory
if (OperatingSystem.IsWindows())
{
var original_dir_info = new DirectoryInfo(os_special_dir);
var restored_dir_info = new DirectoryInfo(os_special_dir.Replace(DATAFOLDER, RESTOREFOLDER));
var original_dir_rules = original_dir_info.GetAccessControl().GetAccessRules(true, false, typeof(System.Security.Principal.NTAccount));
var restored_dir_rules = restored_dir_info.GetAccessControl().GetAccessRules(true, false, typeof(System.Security.Principal.NTAccount));
if (skip_metadata && original_dir_info.Attributes != default_dir_attrs)
Assert.That(original_dir_info.Attributes, Is.Not.EqualTo(restored_dir_info.Attributes), "Directory attributes should not be equal");
else
Assert.That(original_dir_info.Attributes, Is.EqualTo(restored_dir_info.Attributes), "Directory attributes should be equal");
var dir_permissions_equal = CompareCollection(original_dir_rules, restored_dir_rules);
if (restorePermissions && !skip_metadata)
Assert.That(dir_permissions_equal, Is.True, "Directory permissions should be equal");
else
Assert.That(dir_permissions_equal, Is.False, "Directory permissions should not be equal");
var original_file_info = new FileInfo(os_special_file);
var restored_file_info = new FileInfo(os_special_file.Replace(DATAFOLDER, RESTOREFOLDER));
var original_file_rules = original_file_info.GetAccessControl().GetAccessRules(true, false, typeof(System.Security.Principal.NTAccount));
var restored_file_rules = restored_file_info.GetAccessControl().GetAccessRules(true, false, typeof(System.Security.Principal.NTAccount));
if (skip_metadata && original_file_info.Attributes != default_file_attrs)
Assert.That(original_file_info.Attributes, Is.Not.EqualTo(restored_file_info.Attributes), "File attributes should not be equal");
else
Assert.That(original_file_info.Attributes, Is.EqualTo(restored_file_info.Attributes), "File attributes should be equal");
var file_permissions_equal = CompareCollection(original_file_rules, restored_file_rules);
if (restorePermissions && !skip_metadata)
Assert.That(file_permissions_equal, Is.True, "File permissions should be equal");
else
Assert.That(file_permissions_equal, Is.False, "File permissions should not be equal");
}
else
{
var original_dir_info = new DirectoryInfo(os_special_dir);
var restored_dir_info = new DirectoryInfo(os_special_dir.Replace(DATAFOLDER, RESTOREFOLDER));
if (skip_metadata && original_dir_info.Attributes != default_dir_attrs)
Assert.That(original_dir_info.Attributes, Is.Not.EqualTo(restored_dir_info.Attributes), "Directory attributes should not be equal");
else
Assert.That(original_dir_info.Attributes, Is.EqualTo(restored_dir_info.Attributes), "Directory attributes should be equal");
if (restorePermissions && !skip_metadata)
Assert.That(original_dir_info.UnixFileMode, Is.EqualTo(restored_dir_info.UnixFileMode), "Directory permissions should be equal");
else
Assert.That(original_dir_info.UnixFileMode, Is.Not.EqualTo(restored_dir_info.UnixFileMode), "Directory permissions should not be equal");
var original_file_info = new FileInfo(os_special_file);
var restored_file_info = new FileInfo(os_special_file.Replace(DATAFOLDER, RESTOREFOLDER));
if (skip_metadata && original_file_info.Attributes != default_file_attrs)
Assert.That(original_file_info.Attributes, Is.Not.EqualTo(restored_file_info.Attributes), "File attributes should not be equal");
else
Assert.That(original_file_info.Attributes, Is.EqualTo(restored_file_info.Attributes), "File attributes should be equal");
if (restorePermissions && !skip_metadata)
Assert.That(original_file_info.UnixFileMode, Is.EqualTo(restored_file_info.UnixFileMode), "File permissions should be equal");
else
Assert.That(original_file_info.UnixFileMode, Is.Not.EqualTo(restored_file_info.UnixFileMode), "File permissions should not be equal");
}
}
finally
{
// Revert the attributes so the cleanup can proceed
foreach (var dir in dirs)
{
_ = new DirectoryInfo(dir)
{
Attributes = FileAttributes.Normal | FileAttributes.Directory
};
var restored_dir = dir.Replace(DATAFOLDER, RESTOREFOLDER);
if (Directory.Exists(restored_dir))
_ = new DirectoryInfo(restored_dir)
{
Attributes = FileAttributes.Normal | FileAttributes.Directory
};
}
foreach (var attr in attrs)
foreach (var dir in dirs)
{
var file = Path.Combine(dir, $"file_{attr}");
File.SetAttributes(file, FileAttributes.Normal);
var restored_file = file.Replace(DATAFOLDER, RESTOREFOLDER);
if (File.Exists(restored_file))
File.SetAttributes(restored_file, FileAttributes.Normal);
}
// Special dirs can be ignored, as they should only block other
// users from accessing the files
}
}
[SupportedOSPlatform("windows")]
static bool CompareElement(FileSystemAccessRule a, FileSystemAccessRule b) =>
a.IdentityReference.Value == b.IdentityReference.Value &&
a.FileSystemRights == b.FileSystemRights &&
a.AccessControlType == b.AccessControlType;
// Use order-independent comparison since ACL rules may be returned in different orders
[SupportedOSPlatform("windows")]
static bool CompareCollection(AuthorizationRuleCollection a, AuthorizationRuleCollection b) =>
a.Count == b.Count &&
a.Cast<FileSystemAccessRule>().All(ruleA =>
b.Cast<FileSystemAccessRule>().Any(ruleB => CompareElement(ruleA, ruleB)));
[Test]
[Category("Disruption"), Category("Bug")]
public async Task TestSystematicErrors5023Async()
{
// Attempt to recreate other bugs from #5023, but not successful
var testopts = new Dictionary<string, string>(TestOptions)
{
["number-of-retries"] = "0",
["dblock-size"] = "20KB",
["threshold"] = "1",
["keep-versions"] = "5",
["no-encryption"] = "true",
["disable-synthetic-filelist"] = "true"
};
//testopts["rebuild-missing-dblock-files"] = "true";
string target = "file://" + TARGETFOLDER;
string targetError = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
int maxFiles = 10;
List<string> files = new List<string>();
bool failed = false;
long accessCounter = 0;
long errorIdx = 0;
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
++accessCounter;
if (accessCounter >= errorIdx)
{
return true;
}
return false;
};
for (int i = 0; i < maxFiles; ++i)
{
string f = Path.Combine(DATAFOLDER, "f" + i);
TestUtils.WriteTestFile(f, 1024 * 20);
files.Add(f);
}
// Initial backup
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
while (errorIdx < (maxFiles + 2))
{
if (errorIdx % 10 == 0)
{
TestContext.WriteLine("Error index {0}", errorIdx);
}
accessCounter = 0;
try
{
using (var c = new Library.Main.Controller(targetError, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
}
catch (AssertionException) { throw; }
catch { }
Thread.Sleep(1000);
try
{
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
}
catch (UserInformationException e)
{
TestContext.WriteLine("Error at index {0}: HelpID={1}, Message={2}", errorIdx, e.HelpID, e.Message);
if (e.HelpID == "MissingRemoteFiles" || e.HelpID == "ExtraRemoteFiles" || e.HelpID == "DatabaseRepairInProgress")
{
// If a database repair failed, we need to delete the database file
if (e.HelpID == "DatabaseRepairInProgress")
File.Delete(DBFILE);
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.RepairAsync());
}
else if (e.HelpID == "DatabaseTimestampError")
{
// Clock skew detected, wait and retry - this can happen on CI due to timing issues
TestContext.WriteLine("Clock skew detected at index {0}, waiting 10 seconds before continuing", errorIdx);
Thread.Sleep(10000);
}
else
{
TestContext.WriteLine("Unexpected error at index {0}: HelpID={1}, Message={2}", errorIdx, e.HelpID, e.Message);
failed = true;
}
}
Thread.Sleep(1000);
foreach (string f in files)
{
TestUtils.WriteTestFile(f, 1024 * 20);
}
++errorIdx;
}
TestContext.WriteLine("Ran {0} iterations", errorIdx);
Assert.IsFalse(failed);
}
[Test, Sequential]
[Category("Targeted"), Category("Bug"), Category("Non-critical")]
[TestCase(false, true), TestCase(true, true), TestCase(true, false)]
public async Task Issue5038MissingListBlocklistAsync(bool sameVersion, bool blockFirst)
{
// Backup containing the blocklist of a file BEFORE the file causes a dindex with missing blocklist entry
// This is not critical, because it only requires extra block volume downloads
var testopts = new Dictionary<string, string>(TestOptions)
{
["no-encryption"] = "true"
};
string filename = Path.Combine(DATAFOLDER, "file");
// Start with z to process blockfile after file (at least on some systems)
string blockfile = Path.Combine(DATAFOLDER, blockFirst ? "block" : "zblock");
string target = "file://" + TARGETFOLDER;
byte[] block1 = new byte[10 * 1024];
block1.AsSpan().Fill(1);
byte[] block2 = new byte[10 * 1024];
block2.AsSpan().Fill(2);
HashAlgorithm blockhasher = Library.Utility.HashFactory.CreateHasher(new Options(testopts).BlockHashAlgorithm);
var hash1 = blockhasher.ComputeHash(block1, 0, block1.Length);
var hash2 = blockhasher.ComputeHash(block2, 0, block2.Length);
byte[] blockfileContent = hash1.Concat(hash2).ToArray();
TestUtils.WriteFile(blockfile, blockfileContent);
if (!sameVersion)
{
// Backup blockfile first
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
}
byte[] combined = block1.Concat(block2).ToArray();
TestUtils.WriteFile(filename, combined);
// Backup file that would produce blockfile
using (var c = new Library.Main.Controller(target, testopts, null))
TestUtils.AssertResults(await c.BackupAsync(new[] { DATAFOLDER }));
// Recreate database downloads block volume
File.Delete(DBFILE);
using (var c = new Library.Main.Controller(target, testopts, null))
{
var repairResults = await c.RepairAsync();
TestUtils.AssertResults(repairResults);
Assert.IsNull(repairResults.Messages.FirstOrDefault(v => v.Contains("ProcessingRequiredBlocklistVolumes")),
"Blocklist download pass was required");
}
}
[Test]
public async Task Issue6817DollarSignNumberInFilenameBreaksRegexAsync()
{
var filename = "~$1234567891011121314151617181920.txt";
var content = RandomNumberGenerator.GetBytes(1024);
TestUtils.WriteFile(Path.Combine(DATAFOLDER, filename), content);
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null))
TestUtils.AssertResults(await c.BackupAsync([DATAFOLDER]));
// Delete the database
File.Delete(DBFILE);
// Recreate the database, which would fail if the filename is not handled correctly
using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, TestOptions, null))
TestUtils.AssertResults(await c.RepairAsync());
}
}
}