2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-02-28 15:45:30 +01:00
// https://duplicati.com, hello@duplicati.com
2025-05-27 09:40:30 +02: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
2024-02-28 15:45:30 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-27 09:40:30 +02:00
//
// The above copyright notice and this permission notice shall be included in
2024-02-28 15:45:30 +01:00
// all copies or substantial portions of the Software.
2025-05-27 09:40:30 +02: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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2020-12-26 21:20:46 -08:00
using System ;
2020-01-09 20:12:44 -08:00
using System.Collections.Generic ;
2020-12-26 21:20:46 -08:00
using System.Data ;
2020-01-09 20:12:44 -08:00
using System.IO ;
2024-09-09 17:17:24 +02:00
using System.IO.Compression ;
2020-01-09 20:12:44 -08:00
using System.Linq ;
2025-03-11 08:08:28 +01:00
using System.Threading ;
2025-05-27 09:41:07 +02:00
using System.Threading.Tasks ;
2020-07-05 15:00:08 -07:00
using Duplicati.Library.Interface ;
2020-01-09 20:12:44 -08:00
using Duplicati.Library.Main ;
2020-12-26 21:20:46 -08:00
using Duplicati.Library.Main.Database ;
2024-09-10 10:02:54 +02:00
using Duplicati.Library.Main.Volumes ;
2020-12-26 21:20:46 -08:00
using Duplicati.Library.SQLiteHelper ;
2024-09-09 17:17:24 +02:00
using Duplicati.Library.Utility ;
2020-01-09 20:12:44 -08:00
using NUnit.Framework ;
2025-06-13 11:48:24 +02:00
using NUnit.Framework.Legacy ;
using Assert = NUnit . Framework . Legacy . ClassicAssert ;
2020-01-09 20:12:44 -08:00
namespace Duplicati.UnitTest
{
[TestFixture]
public class RepairHandlerTests : BasicSetupHelper
{
2022-05-05 22:30:15 -07:00
[SetUp]
public void SetUp ()
2020-01-09 20:12:44 -08:00
{
2024-09-10 10:02:54 +02:00
File . WriteAllBytes ( Path . Combine ( this . DATAFOLDER , "file" ), new byte [] { 0 });
2020-01-09 20:12:44 -08:00
}
2025-03-10 14:40:13 +01:00
public void ModifyFile ()
{
File . WriteAllBytes ( Path . Combine ( this . DATAFOLDER , "file" ), new byte [] { 1 });
}
2020-12-26 21:20:46 -08:00
[Test]
[Category("RepairHandler")]
2025-04-03 21:05:22 +02:00
[TestCase(150 * 1024)]
// With 10kib blocksize, we can have 10240/32 = 320 hashes
// Test near limits of blocklist to check split blocklist
2025-04-03 21:35:16 +02:00
[TestCase(319 * 10240)]
[TestCase(320 * 10240)]
[TestCase(320 * 10240 + 5)]
[TestCase(320 * 10240 * 2)]
[TestCase(320 * 10240 * 2 + 5)]
[TestCase(320 * 10240 * 3)]
[TestCase(320 * 10240 * 3 + 5)]
2025-04-03 21:05:22 +02:00
public void RepairMissingBlocklistHashes ( int dataSize )
2020-12-26 21:20:46 -08:00
{
2025-04-03 21:05:22 +02:00
byte [] data = new byte [ dataSize ];
2020-12-26 21:20:46 -08:00
Random rng = new Random ();
for ( int k = 0 ; k < 2 ; k ++)
{
rng . NextBytes ( data );
File . WriteAllBytes ( Path . Combine ( this . DATAFOLDER , $"{k}" ), data );
}
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions );
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2024-09-10 13:54:28 +02:00
var backupResults = c . Backup ([ this . DATAFOLDER ]);
2020-12-26 21:20:46 -08:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
}
// Mimic a damaged database that needs to be repaired.
const string selectStatement = @"SELECT BlocksetID, ""Index"", Hash FROM BlocklistHash ORDER BY Hash ASC" ;
List < int > expectedBlocksetIDs = new List < int >();
List < int > expectedIndexes = new List < int >();
List < string > expectedHashes = new List < string >();
2025-06-19 11:41:39 +02:00
using ( IDbConnection connection = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2020-12-26 21:20:46 -08:00
{
// Read the contents of the BlocklistHash table so that we can
// compare them to the contents after the repair operation.
using ( IDbCommand command = connection . CreateCommand ())
{
using ( IDataReader reader = command . ExecuteReader ( selectStatement ))
{
while ( reader . Read ())
{
expectedBlocksetIDs . Add ( reader . GetInt32 ( 0 ));
expectedIndexes . Add ( reader . GetInt32 ( 1 ));
2025-04-03 15:46:20 +02:00
expectedHashes . Add ( reader . ConvertValueToString ( 2 ));
2020-12-26 21:20:46 -08:00
}
}
}
using ( IDbCommand command = connection . CreateCommand ())
{
command . ExecuteNonQuery ( @"DELETE FROM BlocklistHash" );
using ( IDataReader reader = command . ExecuteReader ( selectStatement ))
{
Assert . IsFalse ( reader . Read ());
}
}
}
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IRepairResults repairResults = c . Repair ();
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Count ());
}
List < int > repairedBlocksetIDs = new List < int >();
List < int > repairedIndexes = new List < int >();
List < string > repairedHashes = new List < string >();
2025-06-19 11:41:39 +02:00
using ( IDbConnection connection = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2020-12-26 21:20:46 -08:00
{
using ( IDbCommand command = connection . CreateCommand ())
{
using ( IDataReader reader = command . ExecuteReader ( selectStatement ))
{
while ( reader . Read ())
{
repairedBlocksetIDs . Add ( reader . GetInt32 ( 0 ));
repairedIndexes . Add ( reader . GetInt32 ( 1 ));
2025-04-03 15:46:20 +02:00
repairedHashes . Add ( reader . ConvertValueToString ( 2 ));
2020-12-26 21:20:46 -08:00
}
}
}
}
CollectionAssert . AreEqual ( expectedBlocksetIDs , repairedBlocksetIDs );
CollectionAssert . AreEqual ( expectedIndexes , repairedIndexes );
CollectionAssert . AreEqual ( expectedHashes , repairedHashes );
// A subsequent backup should run without errors.
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2024-09-09 15:27:37 +02:00
var backupResults = c . Backup ([ this . DATAFOLDER ]);
2020-12-26 21:20:46 -08:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
}
}
2020-01-09 20:12:44 -08:00
[Test]
[Category("RepairHandler")]
[TestCase("true")]
[TestCase("false")]
public void RepairMissingIndexFiles ( string noEncryption )
{
2023-10-08 21:39:13 +02:00
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions ) { [ "no-encryption" ] = noEncryption };
2020-01-09 20:12:44 -08:00
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2024-09-09 15:27:37 +02:00
var backupResults = c . Backup ([ this . DATAFOLDER ]);
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
2020-01-09 20:12:44 -08:00
}
2025-05-10 09:47:50 +02:00
var dindexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dindex.*" ). ToArray ();
2020-01-09 21:32:33 -08:00
Assert . Greater ( dindexFiles . Length , 0 );
2024-09-09 15:27:37 +02:00
foreach ( var f in dindexFiles )
2020-01-09 20:12:44 -08:00
{
File . Delete ( f );
}
2024-09-09 15:27:37 +02:00
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
2020-01-09 20:12:44 -08:00
{
2024-09-09 15:27:37 +02:00
var repairResults = c . Repair ();
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Count ());
2020-01-09 20:12:44 -08:00
}
2025-05-10 09:47:50 +02:00
var recreatedIndexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*dindex*" ). ToArray ();
Assert . AreEqual ( dindexFiles . Length , recreatedIndexFiles . Length );
2020-01-09 20:12:44 -08:00
}
2023-10-08 21:39:13 +02:00
2023-11-30 21:45:46 +01:00
[Test]
[Category("RepairHandler"), Category("Targeted")]
public void RepairMissingIndexFilesBlocklist ()
{
// See issue #3202
2024-09-09 15:27:37 +02:00
var options = new Dictionary < string , string >( this . TestOptions )
2023-11-30 21:45:46 +01:00
{
2024-09-09 15:27:37 +02:00
["blocksize"] = "1KB" ,
["no-encryption"] = "true"
2023-11-30 21:45:46 +01:00
};
2024-09-09 15:27:37 +02:00
var filename = Path . Combine ( this . DATAFOLDER , "file" );
2023-11-30 21:45:46 +01:00
using ( var s = File . Create ( filename ))
{
2024-09-09 15:27:37 +02:00
var size = 1024 * 32 + 1 ; // Blocklist size + 1
2023-11-30 21:45:46 +01:00
s . Write ( new byte [ size ], 0 , size );
}
2024-09-09 15:27:37 +02:00
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
2023-11-30 21:45:46 +01:00
{
2024-09-09 15:27:37 +02:00
var backupResults = c . Backup ( new [] { this . DATAFOLDER });
2023-11-30 21:45:46 +01:00
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
using ( var s = File . OpenWrite ( filename ))
{
// Change first byte
s . WriteByte ( 1 );
}
backupResults = c . Backup ( new [] { this . DATAFOLDER });
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
}
2025-05-10 09:47:50 +02:00
var dindexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dindex.*" ). ToArray ();
2023-11-30 21:45:46 +01:00
Assert . Greater ( dindexFiles . Length , 0 );
2024-09-09 15:27:37 +02:00
foreach ( var f in dindexFiles )
2023-11-30 21:45:46 +01:00
{
File . Delete ( f );
}
2024-09-09 15:27:37 +02:00
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
2023-11-30 21:45:46 +01:00
{
2024-09-09 15:27:37 +02:00
var repairResults = c . Repair ();
2020-07-05 15:00:08 -07:00
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Count ());
2020-01-09 20:12:44 -08:00
}
2025-05-10 09:47:50 +02:00
var recreatedIndexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*dindex*" ). ToArray ();
Assert . AreEqual ( dindexFiles . Length , recreatedIndexFiles . Length );
2020-01-09 20:12:44 -08:00
}
2024-09-10 10:02:54 +02:00
2024-09-09 17:17:24 +02:00
[Test]
[Category("RepairHandler"), Category("Targeted")]
public void RecreateWithDefectIndexBlock ()
{
// See issue #3202
var options = new Dictionary < string , string >( this . TestOptions )
{
["blocksize"] = "1KB" ,
["no-encryption"] = "true"
};
var filename = Path . Combine ( this . DATAFOLDER , "file" );
using ( var s = File . Create ( filename ))
{
var size = 1024 * 32 + 1 ; // Blocklist size + 1
s . Write ( new byte [ size ], 0 , size );
}
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var backupResults = c . Backup ( new [] { this . DATAFOLDER });
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
using ( var s = File . OpenWrite ( filename ))
{
// Change first byte
s . WriteByte ( 1 );
}
backupResults = c . Backup ( new [] { this . DATAFOLDER });
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
}
var dindexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*dindex*" ). ToArray ();
Assert . Greater ( dindexFiles . Length , 0 );
// Corrupt the first index file
using ( var tmp = new TempFile ())
{
using ( var zip = new ZipArchive ( File . Open ( tmp , FileMode . Create , FileAccess . ReadWrite ), ZipArchiveMode . Create ))
using ( var sourceZip = new ZipArchive ( File . Open ( dindexFiles [ 0 ], FileMode . Open , FileAccess . ReadWrite )))
{
foreach ( var entry in sourceZip . Entries )
{
using ( var s = entry . Open ())
{
var newEntry = zip . CreateEntry ( entry . FullName );
using ( var d = newEntry . Open ())
{
if ( entry . FullName . StartsWith ( "list/" ))
{
using ( var ms = new MemoryStream ())
{
s . CopyTo ( ms );
ms . Position = 0 ;
ms . WriteByte ( 42 );
ms . Position = 0 ;
ms . CopyTo ( d );
}
}
else
{
s . CopyTo ( d );
}
}
}
}
}
File . Copy ( tmp , dindexFiles [ 0 ], true );
}
// Delete database and recreate
File . Delete ( options [ "dbpath" ]);
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var repairResults = c . Repair ();
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 1 , repairResults . Warnings . Count ());
}
File . Delete ( dindexFiles [ 0 ]);
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var repairResults = c . Repair ();
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Count ());
}
// Delete database and recreate
File . Delete ( options [ "dbpath" ]);
// No errors with recreated index file
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var repairResults = c . Repair ();
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Count ());
}
}
2024-09-10 13:54:28 +02:00
2024-09-10 10:02:54 +02:00
[Test]
[Category("RepairHandler"), Category("Targeted")]
public void AutoCleanupRepairDoesNotLockDatabase ()
{
// See issue #3635, #4631
var options = new Dictionary < string , string >( this . TestOptions )
{
["blocksize"] = "1KB" ,
["no-encryption"] = "true" ,
["auto-cleanup"] = "true"
};
2024-09-10 12:47:19 +02:00
var delaytime = TimeSpan . FromSeconds ( 3 );
2024-09-10 10:02:54 +02:00
var filename = Path . Combine ( this . DATAFOLDER , "file" );
using ( var s = File . Create ( filename ))
s . SetLength ( 1024 * 38 ); // Random size
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var backupResults = c . Backup ([ this . DATAFOLDER ]);
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
Assert . AreEqual ( 0 , backupResults . Warnings . Count ());
}
var dblockFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*dblock*" ). ToArray ();
Assert . Greater ( dblockFiles . Length , 0 );
var sourcename = Path . GetFileName ( dblockFiles . First ());
var p = VolumeBase . ParseFilename ( sourcename );
var guid = VolumeWriterBase . GenerateGuid ();
var time = p . Time . Ticks == 0 ? p . Time : p . Time . AddSeconds ( 1 );
var newname = VolumeBase . GenerateFilename ( p . FileType , p . Prefix , guid , time , p . CompressionModule , p . EncryptionModule );
File . Copy ( Path . Combine ( this . TARGETFOLDER , sourcename ), Path . Combine ( this . TARGETFOLDER , newname ));
2024-09-10 12:47:19 +02:00
System . Threading . Thread . Sleep ( delaytime );
2024-09-10 10:02:54 +02:00
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var backupResults = c . Backup ([ this . DATAFOLDER ]);
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
// 1 extra file + 1 warning
Assert . AreEqual ( 2 , backupResults . Warnings . Count ());
}
// Auto-cleanup should have removed the renamed file
Assert . IsFalse ( File . Exists ( Path . Combine ( this . TARGETFOLDER , newname )));
// Insert the extra file back
File . Copy ( Path . Combine ( this . TARGETFOLDER , sourcename ), Path . Combine ( this . TARGETFOLDER , newname ));
// Delete the database
File . Delete ( options [ "dbpath" ]);
// Recreate with an extra volume
2024-09-10 12:47:19 +02:00
System . Threading . Thread . Sleep ( delaytime );
2024-09-10 10:02:54 +02:00
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var backupResults = c . Backup ([ this . DATAFOLDER ]);
Assert . AreEqual ( 0 , backupResults . Errors . Count ());
// First will recreate (4 files + 1 warning)
Assert . AreEqual ( 5 , backupResults . Warnings . Count ());
}
}
2024-10-29 16:40:06 +01:00
2023-10-08 21:39:13 +02:00
[Test]
[Category("RepairHandler")]
[TestCase("true")]
[TestCase("false")]
2025-05-27 09:41:07 +02:00
public async Task RepairExtraIndexFiles ( string noEncryption )
2023-10-08 21:39:13 +02:00
{
// Extra index files will be added to the database and should have a correct link established
Dictionary < string , string > options = new Dictionary < string , string >( this . TestOptions ) { [ "no-encryption" ] = noEncryption };
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IBackupResults backupResults = c . Backup ( new [] { this . DATAFOLDER });
TestUtils . AssertResults ( backupResults );
}
string [] dindexFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*dindex*" ). ToArray ();
Assert . Greater ( dindexFiles . Length , 0 );
string origFile = dindexFiles . First ();
// Duplicate index file
string dupFile = Path . Combine ( TARGETFOLDER , Path . GetFileName ( origFile ). Replace ( ".dindex" , "1.dindex" ));
File . Copy ( origFile , dupFile );
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
}
// Check database block link
2025-06-19 11:41:39 +02:00
using ( LocalDatabase db = await LocalDatabase . CreateLocalDatabaseAsync ( DBFILE , "Test" , true , null , CancellationToken . None ). ConfigureAwait ( false ))
2023-10-08 21:39:13 +02:00
{
2025-06-12 08:34:29 +02:00
var indexVolumeId = await db
2025-06-19 11:00:24 +02:00
. GetRemoteVolumeID ( Path . GetFileName ( origFile ), CancellationToken . None )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
var duplicateVolumeId = await db
2025-06-19 11:00:24 +02:00
. GetRemoteVolumeID ( Path . GetFileName ( dupFile ), CancellationToken . None )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2023-10-08 21:39:13 +02:00
Assert . AreNotEqual (- 1 , indexVolumeId );
Assert . AreNotEqual (- 1 , duplicateVolumeId );
using ( var cmd = db . Connection . CreateCommand ())
{
2025-04-03 13:58:54 +02:00
var sql = @"SELECT ""BlockVolumeID"" FROM ""IndexBlockLink"" WHERE ""IndexVolumeID"" = @VolumeId" ;
2025-06-12 08:34:29 +02:00
var linkedIndexId = await cmd
. SetCommandAndParameters ( sql )
. SetParameterValue ( "@VolumeId" , indexVolumeId )
2025-06-19 11:00:24 +02:00
. ExecuteScalarInt64Async (- 1 , CancellationToken . None )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
var linkedDuplicateId = await cmd
. SetCommandAndParameters ( sql )
. SetParameterValue ( "@VolumeId" , duplicateVolumeId )
2025-06-19 11:00:24 +02:00
. ExecuteScalarInt64Async (- 1 , CancellationToken . None )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2023-10-08 21:39:13 +02:00
Assert . AreEqual ( linkedIndexId , linkedDuplicateId );
}
}
}
2025-03-10 14:40:13 +01:00
[Test]
[Category("RepairHandler")]
public void RepairMissingDlistFile ()
{
// Make two backups
var options = this . TestOptions ;
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
ModifyFile ();
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
// Find and delete a dlist file
var dlistFile = Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" ). First ();
File . Delete ( dlistFile );
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
Assert . AreEqual ( 2 , Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" ). Count ());
}
}
[Test]
[Category("RepairHandler")]
[TestCase("true")]
[TestCase("false")]
public void RepairMissingDlistVolume ( bool deleteRemoteFile )
{
// Make two backups
var options = this . TestOptions ;
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
ModifyFile ();
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
// Find a dlist file
2025-05-20 22:10:51 +02:00
var dlistFiles = Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" )
. OrderByDescending ( x => x )
. AsEnumerable ();
// Randomly delete the first or last file
if ( Random . Shared . Next ( 2 ) == 0 )
dlistFiles = dlistFiles . Reverse ();
var dlistFile = dlistFiles . First ();
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-04-03 13:58:54 +02:00
using ( var cmd = con . CreateCommand ( "DELETE FROM RemoteVolume WHERE Name = @Name" ))
Assert . AreEqual ( 1 , cmd . SetParameterValue ( "@Name" , Path . GetFileName ( dlistFile )). ExecuteNonQuery ());
2025-03-10 14:40:13 +01:00
2025-05-20 22:10:51 +02:00
if ( deleteRemoteFile )
File . Delete ( dlistFile );
2025-03-10 14:40:13 +01:00
// Should catch this in validation
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
Assert . Throws < DatabaseInconsistencyException >(() => c . Backup ( new [] { this . DATAFOLDER }));
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
Assert . AreEqual ( 2 , Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" ). Count ());
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
}
// Check that the entry was recreated
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-04-03 13:58:54 +02:00
using ( var cmd = con . CreateCommand ( "SELECT COUNT(*) FROM RemoteVolume WHERE Name LIKE '%.dlist.%' AND State != @State" ))
Assert . AreEqual ( 2 , cmd . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ()). ExecuteScalarInt64 (- 1 ));
2025-03-10 14:40:13 +01:00
// Delete the database and check that the result is correct
File . Delete ( options [ "dbpath" ]);
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
IRepairResults repairResults = c . Repair ();
TestUtils . AssertResults ( repairResults );
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
var r = c . Test ( long . MaxValue );
Assert . AreEqual ( 0 , r . Errors . Count ());
Assert . AreEqual ( 0 , r . Warnings . Count ());
Assert . IsFalse ( r . Verifications . Any ( p => p . Value . Any ()));
}
}
[Test]
[Category("RepairHandler")]
public void RepairMissingFilesetVolume ()
{
// Make two backups
var options = this . TestOptions ;
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2025-03-11 08:08:28 +01:00
// Make room for a new backup
Thread . Sleep ( 2000 );
2025-03-10 14:40:13 +01:00
ModifyFile ();
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
// Remove a fileset
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-03-10 14:40:13 +01:00
using ( var cmd = con . CreateCommand ())
{
var filesetId = cmd . ExecuteScalarInt64 ( "SELECT Id FROM Fileset ORDER BY Id DESC LIMIT 1" );
2025-04-03 22:27:13 +02:00
Assert . AreEqual ( 1 , cmd . SetCommandAndParameters ( "DELETE FROM Fileset WHERE Id = @FilesetId" )
. SetParameterValue ( "@FilesetId" , filesetId )
. ExecuteNonQuery ());
// No longer needed because the recreate will wipe the table before restoring
// cmd.SetCommandAndParameters("DELETE FROM FilesetEntry WHERE FilesetId = @FilesetId")
// .SetParameterValue("@FilesetId", filesetId)
// .ExecuteNonQuery();
2025-03-10 14:40:13 +01:00
}
// Should catch this in validation
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
Assert . Throws < DatabaseInconsistencyException >(() => c . Backup ( new [] { this . DATAFOLDER }));
2025-03-11 10:45:44 +01:00
// Since we have removed the entry that is being checked here, we need to disable the check
var repairOptions = new Dictionary < string , string >( options ) { [ "repair-ignore-outdated-database" ] = "true" };
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , repairOptions , null ))
2025-03-10 14:40:13 +01:00
{
IRepairResults repairResults = c . Repair ();
2025-03-11 17:00:14 +01:00
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Where ( x => x . IndexOf ( "RemoteFilesNewerThanLocalDatabase" , StringComparison . OrdinalIgnoreCase ) < 0 ). Count ());
2025-03-10 14:40:13 +01:00
Assert . AreEqual ( 2 , Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" ). Count ());
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
}
// Check that entry was recreated
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-03-10 14:40:13 +01:00
using ( var cmd = con . CreateCommand ())
Assert . AreEqual ( 2 , cmd . ExecuteScalarInt64 ( "SELECT COUNT(*) FROM Fileset" ));
}
2025-03-20 15:59:14 +01:00
[Test]
[Category("RepairHandler")]
public void ManufactureMissingFiles ()
{
// Make two backups
var options = this . TestOptions ;
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
// Make room for a new backup
Thread . Sleep ( 2000 );
ModifyFile ();
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
// Remove a fileset
long filesetEntries ;
2025-03-20 16:12:28 +01:00
long fileLookupEntries ;
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-03-20 15:59:14 +01:00
using ( var cmd = con . CreateCommand ())
{
filesetEntries = cmd . ExecuteScalarInt64 ( "SELECT COUNT(*) FROM FilesetEntry" );
2025-03-20 16:12:28 +01:00
fileLookupEntries = cmd . ExecuteScalarInt64 ( "SELECT COUNT(*) FROM FileLookup" );
var fileId = cmd . ExecuteScalarInt64 ( "SELECT FileId FROM FilesetEntry INNER JOIN FileLookup ON FilesetEntry.FileID = FileLookup.Id WHERE FileLookup.BlocksetID != -100 ORDER BY FilesetId DESC LIMIT 1" );
2025-04-03 13:58:54 +02:00
Assert . AreEqual ( 1 , cmd . SetCommandAndParameters ( "DELETE FROM FileLookup WHERE Id = @FileId" ). SetParameterValue ( "@FileId" , fileId ). ExecuteNonQuery ());
2025-03-20 15:59:14 +01:00
}
// Should catch this in validation
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
Assert . Throws < DatabaseInconsistencyException >(() => c . Backup ( new [] { this . DATAFOLDER }));
using ( Controller c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
2025-03-25 11:56:52 +01:00
var repairResults = c . Repair ();
2025-03-20 15:59:14 +01:00
Assert . AreEqual ( 0 , repairResults . Errors . Count ());
Assert . AreEqual ( 0 , repairResults . Warnings . Where ( x => x . IndexOf ( "RemoteFilesNewerThanLocalDatabase" , StringComparison . OrdinalIgnoreCase ) < 0 ). Count ());
Assert . AreEqual ( 2 , Directory . EnumerateFiles ( this . TARGETFOLDER , "*.dlist.*" ). Count ());
Assert . AreEqual ( 2 , c . List (). Filesets . Count ());
}
// Check that entry was recreated
2025-06-19 11:41:39 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-03-20 15:59:14 +01:00
using ( var cmd = con . CreateCommand ())
2025-03-20 16:12:28 +01:00
{
2025-03-20 15:59:14 +01:00
Assert . AreEqual ( filesetEntries , cmd . ExecuteScalarInt64 ( "SELECT COUNT(*) FROM FilesetEntry" ));
2025-03-20 16:12:28 +01:00
Assert . AreEqual ( fileLookupEntries , cmd . ExecuteScalarInt64 ( "SELECT COUNT(*) FROM FileLookup" ));
}
2025-03-20 15:59:14 +01:00
}
2025-06-16 16:35:07 +02:00
[Test]
[Category("RepairHandler")]
public void RepairReplacesZeroLengthMetadata ()
{
var options = new Dictionary < string , string >( this . TestOptions );
File . WriteAllText ( Path . Combine ( this . DATAFOLDER , "a.txt" ), "a" );
File . WriteAllText ( Path . Combine ( this . DATAFOLDER , "b.txt" ), "b" );
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
TestUtils . AssertResults ( c . Backup ( new [] { this . DATAFOLDER }));
2025-06-19 12:24:22 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-06-16 16:35:07 +02:00
using ( var cmd = con . CreateCommand ())
{
var lookupId = cmd . ExecuteScalarInt64 ( "SELECT ID FROM FileLookup LIMIT 1" );
var metaId = cmd . SetCommandAndParameters ( "SELECT MetadataID FROM FileLookup WHERE ID = @Id" )
. SetParameterValue ( "@Id" , lookupId ). ExecuteScalarInt64 (- 1 );
var blocksetId = cmd . SetCommandAndParameters ( "SELECT BlocksetID FROM Metadataset WHERE ID = @Id" )
. SetParameterValue ( "@Id" , metaId ). ExecuteScalarInt64 (- 1 );
cmd . SetCommandAndParameters ( "UPDATE Blockset SET Length = 0 WHERE ID = @Id" )
. SetParameterValue ( "@Id" , blocksetId ). ExecuteNonQuery ();
}
using ( var c = new Controller ( "file://" + this . TARGETFOLDER , options , null ))
{
var res = c . Repair ();
Assert . AreEqual ( 0 , res . Errors . Count ());
Assert . AreEqual ( 0 , res . Warnings . Count ());
}
2025-06-19 12:24:22 +02:00
using ( var con = SQLiteLoader . LoadConnection ( options [ "dbpath" ]))
2025-06-16 16:35:07 +02:00
using ( var cmd = con . CreateCommand ())
{
var remaining = cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM Metadataset JOIN Blockset ON Metadataset.BlocksetID = Blockset.ID WHERE Blockset.Length = 0" );
Assert . AreEqual ( 0 , remaining , "Zero-length metadata should have been replaced" );
}
}
2020-01-09 20:12:44 -08:00
}
}