2025-03-26 15:51:27 +01:00
// Copyright (C) 2025, The Duplicati Team
2025-01-07 09:40:39 +01:00
// https://duplicati.com, hello@duplicati.com
2025-02-10 13:44:21 +01:00
//
// 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
2025-01-07 09:40:39 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-02-10 13:44:21 +01:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-07 09:40:39 +01:00
// all copies or substantial portions of the Software.
2025-02-10 13:44:21 +01:00
//
// 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
2025-01-07 09:40:39 +01:00
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.DynamicLoader ;
2024-11-01 11:14:46 +01:00
using Duplicati.Library.Interface ;
2023-10-05 22:49:51 +02:00
using Duplicati.Library.Main ;
2025-08-06 10:15:13 +02:00
using Microsoft.Data.Sqlite ;
2023-10-05 02:59:19 +02:00
using NUnit.Framework ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2026-01-30 08:26:34 +01:00
using System.Runtime.Versioning ;
2025-03-25 10:05:44 +01:00
using System.Security.AccessControl ;
2023-10-05 22:49:51 +02:00
using System.Security.Cryptography ;
2025-03-26 09:20:29 +01:00
using System.Security.Principal ;
2023-10-05 02:59:19 +02:00
using System.Threading ;
2025-06-13 11:48:24 +02:00
using Assert = NUnit . Framework . Legacy . ClassicAssert ;
2023-10-05 02:59:19 +02:00
namespace Duplicati.UnitTest
{
public class IssueTests : BasicSetupHelper
{
[Test]
2023-10-08 21:51:21 +02:00
[Category("Targeted")]
2023-10-05 22:49:51 +02:00
public void Issue5023ReferencedFileMissing ([ Values ] bool compactBeforeRecreate )
2023-10-05 02:59:19 +02:00
{
// 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
2023-10-05 22:49:51 +02:00
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"
};
2023-10-05 02:59:19 +02:00
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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
// Sleep to ensure timestamps are different
Thread . Sleep ( 1000 );
// Fail after index file put
target = new DeterministicErrorBackend (). ProtocolKey + "://" + TARGETFOLDER ;
string interruptedName = "" ;
2025-02-20 11:00:28 +01:00
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
2023-10-05 02:59:19 +02:00
{
// Fail dindex upload
2025-02-20 11:00:28 +01:00
if ( action == DeterministicErrorBackend . BackendAction . PutAfter && remotename . Contains ( "dindex" ))
2023-10-05 02:59:19 +02:00
{
interruptedName = remotename ;
return true ;
}
return false ;
};
2024-11-01 11:14:46 +01:00
BackendLoader . AddBackend ( new DeterministicErrorBackend ());
2023-10-05 02:59:19 +02:00
string file2 = Path . Combine ( DATAFOLDER , "f2" );
TestUtils . WriteTestFile ( file2 , filesize );
2024-11-01 11:14:46 +01:00
var uploadEx = Assert . Catch (() =>
2023-10-05 02:59:19 +02:00
{
using ( var c = new Library . Main . Controller ( target , testopts , null ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
}
});
2024-11-01 11:14:46 +01:00
while ( uploadEx is AggregateException && uploadEx . InnerException is not null )
uploadEx = uploadEx . InnerException ;
Assert . That ( uploadEx , Is . TypeOf < DeterministicErrorBackend . DeterministicErrorBackendException >());
2023-10-05 02:59:19 +02:00
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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
// 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 )
{
2025-03-11 08:08:28 +01:00
// Make room for a backup
Thread . Sleep ( 2000 );
2023-10-05 02:59:19 +02:00
// Delete f2 and compact
File . Delete ( file2 );
2025-03-11 08:08:28 +01:00
using ( var c = new Controller ( target , testopts , null ))
2023-10-05 02:59:19 +02:00
{
2025-03-11 08:08:28 +01:00
var backupResults = c . Backup ( new [] { DATAFOLDER });
2023-10-05 02:59:19 +02:00
TestUtils . AssertResults ( backupResults );
ICompactResults compactResults = c . Compact ();
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 ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
}
}
2023-10-05 22:49:51 +02:00
2025-01-05 11:52:12 +01:00
[Test]
[Category("Restore"), Category("Bug")]
public void Issue5825RestoreNoOverwrite ([ 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 ()
};
2025-01-05 12:17:16 +01:00
int test_filesize = 1024 ;
2025-01-05 11:52:12 +01:00
2025-03-19 21:42:08 +01:00
// TODO check if the original files are untocuched
2025-01-05 12:28:08 +01:00
2025-01-05 11:52:12 +01:00
var original_dir = Path . Combine ( DATAFOLDER , "some_original_dir" );
Directory . CreateDirectory ( original_dir );
string f0 = Path . Combine ( original_dir , "some_file" );
2025-01-05 12:17:16 +01:00
TestUtils . WriteTestFile ( f0 , test_filesize );
2025-01-05 11:52:12 +01:00
// Backup the files
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// Attempt to restore the file
testopts [ "restore-path" ] = RESTOREFOLDER ;
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
var restoreResults = c . Restore ([ 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
2025-01-05 12:17:16 +01:00
TestUtils . WriteTestFile ( f1 , test_filesize );
2025-01-05 11:52:12 +01:00
// Restore the file again, with overwrite.
testopts [ "overwrite" ] = "true" ;
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
var restoreResults = c . Restore ([ 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 = c . Restore ([ 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
2025-01-05 12:17:16 +01:00
TestUtils . WriteTestFile ( f1 , test_filesize );
2025-01-05 12:28:08 +01:00
var f1_original = File . ReadAllBytes ( f1 );
2025-01-05 11:52:12 +01:00
// Restore the file again, without overwrite.
testopts [ "overwrite" ] = "false" ;
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
var restoreResults = c . Restore ([ f0 ]);
Assert . That ( restoreResults . RestoredFiles , Is . EqualTo ( 1 ), "File should have been restored" );
}
2025-01-05 12:28:08 +01:00
// Verify that f1 is still the same
Assert . That ( File . ReadAllBytes ( f1 ), Is . EqualTo ( f1_original ), "The first restored file should remain untouched" );
2025-01-05 11:52:12 +01:00
// 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
2025-01-05 12:17:16 +01:00
TestUtils . WriteTestFile ( f2 , test_filesize );
2025-01-05 12:28:08 +01:00
var f2_original = File . ReadAllBytes ( f2 );
2025-01-05 11:52:12 +01:00
// Restore the file again, without overwrite.
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
var restoreResults = c . Restore ([ f0 ]);
Assert . That ( restoreResults . RestoredFiles , Is . EqualTo ( 1 ), "File should have been restored" );
}
2025-01-05 12:28:08 +01:00
// 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" );
2025-01-05 11:52:12 +01:00
// 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" );
2025-01-05 12:15:43 +01:00
// 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 = c . Restore ([ f0 ]);
Assert . That ( restoreResults . RestoredFiles , Is . EqualTo ( 0 ), "File should not have been restored" );
}
2025-01-05 12:28:08 +01:00
// Verify that f1 is still untouched
Assert . That ( File . ReadAllBytes ( f1 ), Is . EqualTo ( f1_original ), "The first restored file should remain untouched" );
2025-01-05 11:52:12 +01:00
}
2025-01-03 20:34:45 +01:00
[Test]
[Category("Restore"), Category("Bug")]
public void Issue5826RestoreMissingFolder ([ 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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// 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 = c . Restore ([ f ]);
2025-01-05 11:54:38 +01:00
Assert . That ( restoreResults . RestoredFiles , Is . EqualTo ( 1 ), "File should have been restored." );
2025-01-03 20:34:45 +01:00
Assert . That ( restoreResults . Warnings . Count (), Is . EqualTo ( compressRestorePaths ? 0 : 1 ), "Warning should be generated for missing folder" );
}
}
2025-01-17 12:29:11 +01:00
[Test]
2025-02-10 13:44:21 +01:00
[Category("Restore"), Category("Bug")]
2025-01-17 12:34:52 +01:00
public void Issue5886RestoreModifiedMiddleBlock ()
2025-01-17 12:29:11 +01:00
{
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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
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 = c . Restore ([ 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" );
}
}
}
2025-02-10 13:44:21 +01:00
[Test]
[Category("Restore"), Category("Bug")]
public void Issue5957RestoreModifiedBlockUseLocalBlocks ([ 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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
var original_contents = File . ReadAllBytes ( f );
using ( var stream = File . Open ( f , FileMode . Open , FileAccess . ReadWrite ))
{
stream . Seek ( 0 , SeekOrigin . Begin );
byte [] buffer = new byte [ 1 ];
2025-11-14 16:00:06 +01:00
if ( stream . Read ( buffer , 0 , 1 ) < 1 )
throw new InvalidOperationException ( "Failed to read from test file" );
2025-02-10 13:44:21 +01:00
buffer [ 0 ] = ( byte )~ buffer [ 0 ];
stream . Seek ( 0 , SeekOrigin . Begin );
stream . Write ( 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 = c . Restore ([ 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 );
2025-02-10 14:02:36 +01:00
Assert . That ( files . Length , Is . EqualTo ( 2 ), "There should be two files in the folder" );
2025-02-10 13:44:21 +01:00
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" );
}
2025-01-17 12:29:11 +01:00
2025-03-24 13:39:20 +01:00
[Test]
[Category("Restore"), Category("Bug")]
public void Issue6068FolderMetadata ([ 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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// Sleep to ensure timestamps are different
System . Threading . Tasks . Task . Delay ( 1000 ). Wait ();
// 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 = c . Restore ([]);
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" );
}
2025-08-06 10:15:13 +02:00
[Test]
[Category("Test"), Category("Bug")]
2025-08-06 10:20:38 +02:00
public void Issue6459HashNullInTest ()
2025-08-06 10:15:13 +02:00
{
// 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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// Set one of the hashes in the database to null
{
using var con = new SqliteConnection ( $"Data source={DBFILE};Pooling=false" );
con . Open ();
using var cmd = con . CreateCommand ();
using var transaction = con . BeginTransaction ();
cmd . Transaction = transaction ;
cmd . CommandText = @"UPDATE Remotevolume SET Hash = NULL WHERE Name LIKE '%.dblock.%'" ;
cmd . ExecuteNonQuery ();
transaction . Commit ();
}
// 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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// 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 ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
}
// Double check that the null hash is not present anymore
{
using var con = new SqliteConnection ( $"Data source={DBFILE};Pooling=false" );
con . Open ();
using var cmd = con . CreateCommand ();
cmd . CommandText = @"SELECT COUNT(*) FROM Remotevolume WHERE Hash IS NULL" ;
var count = ( long ) cmd . ExecuteScalar ();
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 = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
}
2025-03-25 10:05:44 +01:00
[Test]
[Category("Restore"), Category("Bug")]
public void Issue6068FileAndFolderAttributesAndPermissions ([ 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 ,
2025-03-26 15:51:27 +01:00
// TODO The Archive attribute has been disabled for now, as the CI doesn't behave as expected, but it works locally.
2025-03-26 15:05:21 +01:00
//FileAttributes.Archive
2025-03-25 10:05:44 +01:00
};
// 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 );
2025-03-25 18:16:32 +01:00
var default_dir_attrs = new DirectoryInfo ( os_special_dir ). Attributes ;
2025-03-25 10:05:44 +01:00
var os_special_file = Path . Combine ( os_special_dir , "os_special_file" );
TestUtils . WriteTestFile ( os_special_file , 1024 );
2025-03-25 18:16:32 +01:00
var default_file_attrs = File . GetAttributes ( os_special_file );
2025-03-25 10:05:44 +01:00
if ( OperatingSystem . IsWindows ())
{
// Set only the current user to have access to both the file and the directory
2025-03-25 18:16:32 +01:00
var dir_info = new DirectoryInfo ( os_special_dir );
var rules_dir = dir_info . GetAccessControl ();
rules_dir . GetAccessRules ( true , true , typeof ( System . Security . Principal . NTAccount ));
2025-03-25 10:05:44 +01:00
rules_dir . AddAccessRule ( new FileSystemAccessRule ( Environment . UserName , FileSystemRights . FullControl , AccessControlType . Allow ));
2025-03-26 09:20:29 +01:00
rules_dir . AddAccessRule ( new FileSystemAccessRule ( new SecurityIdentifier ( WellKnownSidType . WorldSid , null ), FileSystemRights . ReadAndExecute , AccessControlType . Allow ));
2025-03-25 18:16:32 +01:00
dir_info . SetAccessControl ( rules_dir );
var file_info = new FileInfo ( os_special_file );
var rules_file = file_info . GetAccessControl ();
2025-03-25 10:05:44 +01:00
rules_file . AddAccessRule ( new FileSystemAccessRule ( Environment . UserName , FileSystemRights . FullControl , AccessControlType . Allow ));
2025-03-26 09:20:29 +01:00
rules_file . AddAccessRule ( new FileSystemAccessRule ( new SecurityIdentifier ( WellKnownSidType . WorldSid , null ), FileSystemRights . ReadAndExecute , AccessControlType . Allow ));
2025-03-25 18:16:32 +01:00
file_info . SetAccessControl ( rules_file );
2025-03-25 10:05:44 +01:00
}
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 ))
{
IBackupResults backupResults = c . Backup ([ DATAFOLDER ]);
TestUtils . AssertResults ( backupResults );
}
// Restore the files
testopts [ "restore-path" ] = RESTOREFOLDER ;
using ( var c = new Library . Main . Controller ( "file://" + TARGETFOLDER , testopts , null ))
{
var restoreResults = c . Restore ([]);
}
// 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 ;
2025-03-26 04:10:31 +01:00
if ( skip_metadata && original_attrs != default_dir_attrs )
2025-03-26 06:20:45 +01:00
Assert . That ( original_attrs , Is . Not . EqualTo ( restored_attrs ), $"Directory attributes should not be equal for directory: {dir}" );
2025-03-25 10:05:44 +01:00
else
2025-03-26 06:20:45 +01:00
Assert . That ( original_attrs , Is . EqualTo ( restored_attrs ), $"Directory attributes should be equal for directory: {dir}" );
2025-03-25 10:05:44 +01:00
}
// Check the file attributes
foreach ( var attr in attrs )
foreach ( var dir in dirs )
{
2025-03-26 14:21:40 +01:00
var filename = $"file_{attr}" ;
var original_file = Path . Combine ( dir , filename );
2025-03-25 10:05:44 +01:00
var restored_file = original_file . Replace ( DATAFOLDER , RESTOREFOLDER );
2025-03-26 15:51:27 +01:00
// TODO we ignore the Archive attribute, as the CI sets this sporadically
2025-03-26 15:51:15 +01:00
var original_attrs = File . GetAttributes ( original_file );
2025-03-25 10:05:44 +01:00
var restored_attrs = File . GetAttributes ( restored_file );
2025-03-26 04:10:31 +01:00
if ( skip_metadata && original_attrs != default_file_attrs )
2025-03-26 15:51:15 +01:00
Assert . That ( original_attrs & ~ FileAttributes . Archive , Is . Not . EqualTo ( restored_attrs & ~ FileAttributes . Archive ), $"File attributes should not be equal for original file: {original_file}" );
2025-03-25 10:05:44 +01:00
else
2025-03-26 15:51:15 +01:00
Assert . That ( original_attrs & ~ FileAttributes . Archive , Is . EqualTo ( restored_attrs & ~ FileAttributes . Archive ), $"File attributes should be equal for original file: {original_file}" );
2025-03-25 10:05:44 +01:00
}
// 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 , true , typeof ( System . Security . Principal . NTAccount ));
var restored_dir_rules = restored_dir_info . GetAccessControl (). GetAccessRules ( true , true , typeof ( System . Security . Principal . NTAccount ));
2025-03-25 18:16:32 +01:00
if ( skip_metadata && original_dir_info . Attributes != default_dir_attrs )
2025-03-25 10:05:44 +01:00
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" );
2026-01-30 08:26:34 +01:00
var dir_permissions_equal = CompareCollection ( original_dir_rules , restored_dir_rules );
2025-03-25 10:05:44 +01:00
if ( restorePermissions && ! skip_metadata )
2025-03-25 18:16:32 +01:00
Assert . That ( dir_permissions_equal , Is . True , "Directory permissions should be equal" );
2025-03-25 10:05:44 +01:00
else
2025-03-25 18:16:32 +01:00
Assert . That ( dir_permissions_equal , Is . False , "Directory permissions should not be equal" );
2025-03-25 10:05:44 +01:00
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 , true , typeof ( System . Security . Principal . NTAccount ));
var restored_file_rules = restored_file_info . GetAccessControl (). GetAccessRules ( true , true , typeof ( System . Security . Principal . NTAccount ));
2025-03-25 18:16:32 +01:00
if ( skip_metadata && original_file_info . Attributes != default_file_attrs )
2025-03-25 10:05:44 +01:00
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" );
2026-01-30 08:26:34 +01:00
var file_permissions_equal = CompareCollection ( original_file_rules , restored_file_rules );
2025-03-25 10:05:44 +01:00
if ( restorePermissions && ! skip_metadata )
2025-03-25 18:16:32 +01:00
Assert . That ( file_permissions_equal , Is . True , "File permissions should be equal" );
2025-03-25 10:05:44 +01:00
else
2025-03-25 18:16:32 +01:00
Assert . That ( file_permissions_equal , Is . False , "File permissions should not be equal" );
2025-03-25 10:05:44 +01:00
}
else
{
var original_dir_info = new DirectoryInfo ( os_special_dir );
var restored_dir_info = new DirectoryInfo ( os_special_dir . Replace ( DATAFOLDER , RESTOREFOLDER ));
2025-03-25 18:16:32 +01:00
if ( skip_metadata && original_dir_info . Attributes != default_dir_attrs )
2025-03-25 10:05:44 +01:00
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 ));
2025-03-25 18:16:32 +01:00
if ( skip_metadata && original_file_info . Attributes != default_file_attrs )
2025-03-25 10:05:44 +01:00
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
}
}
2026-01-30 08:26:34 +01:00
[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 )));
2025-03-25 10:05:44 +01:00
2023-10-05 22:49:51 +02:00
[Test]
2024-10-31 16:51:55 +01:00
[Category("Disruption"), Category("Bug")]
public void TestSystematicErrors5023 ()
2023-10-05 22:49:51 +02:00
{
// 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 ;
2025-02-20 11:00:28 +01:00
DeterministicErrorBackend . ErrorGenerator = ( DeterministicErrorBackend . BackendAction action , string remotename ) =>
2023-10-05 22:49:51 +02:00
{
++ 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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
}
catch ( AssertionException ) { throw ; }
catch { }
Thread . Sleep ( 1000 );
try
{
using ( var c = new Library . Main . Controller ( target , testopts , null ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
}
catch ( UserInformationException e )
{
2026-01-30 08:38:13 +01:00
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" )
2023-10-05 22:49:51 +02:00
{
2026-01-30 08:38:13 +01:00
// If a database repair failed, we need to delete the database file
if ( e . HelpID == "DatabaseRepairInProgress" )
File . Delete ( DBFILE );
2023-10-05 22:49:51 +02:00
using ( var c = new Library . Main . Controller ( target , testopts , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
}
}
2026-01-30 08:38:13 +01:00
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 );
}
2026-01-28 21:15:21 +01:00
else
{
2026-01-30 08:38:13 +01:00
TestContext . WriteLine ( "Unexpected error at index {0}: HelpID={1}, Message={2}" , errorIdx , e . HelpID , e . Message );
2026-01-28 21:15:21 +01:00
failed = true ;
}
2023-10-05 22:49:51 +02:00
}
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]
2024-10-31 13:42:07 +01:00
[Category("Targeted"), Category("Bug"), Category("Non-critical")]
2023-10-05 22:49:51 +02:00
[TestCase(false, true), TestCase(true, true), TestCase(true, false)]
public void Issue5038MissingListBlocklist ( 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 ];
2025-06-11 16:34:22 +02:00
block1 . AsSpan (). Fill ( 1 );
2023-10-05 22:49:51 +02:00
byte [] block2 = new byte [ 10 * 1024 ];
2025-06-11 16:34:22 +02:00
block2 . AsSpan (). Fill ( 2 );
2023-10-05 22:49:51 +02:00
2024-05-11 23:37:21 +02:00
HashAlgorithm blockhasher = Library . Utility . HashFactory . CreateHasher ( new Options ( testopts ). BlockHashAlgorithm );
2023-10-05 22:49:51 +02:00
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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
}
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 ))
{
IBackupResults backupResults = c . Backup ( new [] { DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
// Recreate database downloads block volume
File . Delete ( DBFILE );
using ( var c = new Library . Main . Controller ( target , testopts , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
Assert . IsNull ( repairResults . Messages . FirstOrDefault ( v => v . Contains ( "ProcessingRequiredBlocklistVolumes" )),
"Blocklist download pass was required" );
}
}
2023-10-05 02:59:19 +02:00
}
}