// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Data; using System.Collections.Generic; using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Database { internal class LocalRepairDatabase : LocalDatabase { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalRepairDatabase)); /// /// Creates a new local repair database /// /// The path to the database /// The page cache size public LocalRepairDatabase(string path, long pagecachesize) : base(path, "Repair", true, pagecachesize) { } /// /// Creates a new local repair database from an existing local database /// /// The existing local database public LocalRepairDatabase(LocalDatabase db) : base(db) { } /// /// Gets the fileset ID from the remote name /// /// The remote name of the fileset /// An optional transaction /// >The fileset ID public (long FilesetId, DateTime Time, bool IsFullBackup) GetFilesetFromRemotename(string filelist, IDbTransaction? transaction) { using (var cmd = m_connection.CreateCommand(transaction)) { var rd = cmd.SetCommandAndParameters(@"SELECT ""Fileset"".""ID"", ""Fileset"".""Timestamp"", ""Fileset"".""IsFullBackup"" FROM ""Fileset"",""RemoteVolume"" WHERE ""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""RemoteVolume"".""Name"" = @Name") .SetParameterValue("@Name", filelist) .ExecuteReader(); if (!rd.Read()) throw new Exception($"No such remote file: {filelist}"); return (rd.ConvertValueToInt64(0, -1), ParseFromEpochSeconds(rd.ConvertValueToInt64(1)).ToLocalTime(), rd.GetInt32(2) == BackupType.FULL_BACKUP); } } /// /// Moves entries in the FilesetEntry table from previous fileset to current fileset /// /// Current fileset ID /// Source fileset ID /// The transaction public void MoveFilesFromFileset(long filesetid, long prevFilesetId, IDbTransaction transaction) { if (filesetid <= 0) throw new ArgumentException("filesetid must be > 0"); if (prevFilesetId <= 0) throw new ArgumentException("prevId must be > 0"); using var cmd = m_connection.CreateCommand(transaction); cmd.SetCommandAndParameters(@"UPDATE ""FilesetEntry"" SET ""FilesetID"" = @CurrentFilesetId WHERE ""FilesetID"" = @PreviousFilesetId ") .SetParameterValue("@CurrentFilesetId", filesetid) .SetParameterValue("@PreviousFilesetId", prevFilesetId) .ExecuteNonQuery(); } /// /// Gets the list of index files that reference a given block file /// /// The block file ID /// >A list of index file IDs public IEnumerable GetIndexFilesReferencingBlockFile(long blockfileid, IDbTransaction? transaction) { using var cmd = m_connection.CreateCommand(transaction, @"SELECT ""RemoteVolume"".""Name"" FROM ""RemoteVolume"", ""IndexBlockLink"" WHERE ""IndexBlockLink"".""BlockVolumeID"" = @BlockFileId AND ""RemoteVolume"".""ID"" = ""IndexBlockLink"".""IndexVolumeID"" AND ""RemoteVolume"".""Type"" = @Type") .SetParameterValue("@BlockFileId", blockfileid) .SetParameterValue("@Type", RemoteVolumeType.Index.ToString()); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return rd.ConvertValueToString(0) ?? throw new Exception("RemoteVolume name was null"); } /// /// Gets a list of filesets that are missing files /// /// An optional transaction /// A list of fileset IDs and timestamps public IEnumerable> GetFilesetsWithMissingFiles(IDbTransaction transaction) { using (var cmd = m_connection.CreateCommand(transaction)) using (var rd = cmd.ExecuteReader(@"SELECT ID, Timestamp FROM Fileset WHERE ID IN (SELECT FilesetID FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup""))")) while (rd.Read()) { yield return new KeyValuePair( rd.ConvertValueToInt64(0), ParseFromEpochSeconds(rd.ConvertValueToInt64(1)).ToLocalTime() ); } } /// /// Deletes all fileset entries for a given fileset /// /// The fileset ID /// An optional transaction /// The number of deleted entries public int DeleteFilesetEntries(long filesetid, IDbTransaction? transaction = null) { using (var cmd = m_connection.CreateCommand(transaction)) return cmd.SetCommandAndParameters(@"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" = @FilesetId") .SetParameterValue("@FilesetId", filesetid) .ExecuteNonQuery(); } private class RemoteVolume : IRemoteVolume { public string Name { get; private set; } public string Hash { get; private set; } public long Size { get; private set; } public RemoteVolume(string name, string hash, long size) { this.Name = name; this.Hash = hash; this.Size = size; } } public IEnumerable GetBlockVolumesFromIndexName(string indexName) { using var cmd = m_connection.CreateCommand(@"SELECT ""Name"", ""Hash"", ""Size"" FROM ""RemoteVolume"" WHERE ""ID"" IN (SELECT ""BlockVolumeID"" FROM ""IndexBlockLink"" WHERE ""IndexVolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = @Name)) ") .SetParameterValue("@Name", indexName); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return new RemoteVolume(rd.ConvertValueToString(0) ?? "", rd.ConvertValueToString(1) ?? "", rd.ConvertValueToInt64(2)); } /// /// A single block with source data for repair /// /// The block hash /// The block size /// The file that contains the block /// The offset of the block in the file public sealed record BlockWithSourceData(string Hash, long Size, string File, long Offset); /// /// A single block with metadata source data for repair /// /// The block hash /// The block size /// The path of the file or directory that contains the block public sealed record BlockWithMetadataSourceData(string Hash, long Size, string Path); /// /// A single blocklist hash entry /// /// The blockset id /// The hash of the blocklist entry (when done) /// The total length of the blockset /// The index of the blocklist hash /// The index of the block in the blockset /// The hash of the entry public sealed record BlocklistHashesEntry( long BlocksetId, string BlocklistHash, long BlocklistHashLength, long BlocklistHashIndex, int Index, string Hash); /// /// Helper interface for the missing block list /// public interface IMissingBlockList : IDisposable { /// /// Registers a block as restored /// /// The block hash /// The block size /// The volume ID of the new target volume /// The restored blocks bool SetBlockRestored(string hash, long size, long volumeId); /// /// Gets the list of files that contains missing blocks /// /// The blocksize setting /// A list of files with missing blocks IEnumerable GetSourceFilesWithBlocks(long blocksize); /// /// Gets a list for filesystem entries that contain missing blocks in metadata /// /// A list of filesystem entries with missing blocks IEnumerable GetSourceItemsWithMetadataBlocks(); /// /// Gets missing blocklist hashes /// /// The number of hashes for each block /// >A list of blocklist hashes IEnumerable GetBlocklistHashes(long hashesPerBlock); /// /// Gets the number of missing blocks /// /// >The number of missing blocks long GetMissingBlockCount(); /// /// Gets all the filesets that are affected by missing blocks /// /// >A list of filesets IEnumerable GetFilesetsUsingMissingBlocks(); /// /// Gets a list of remote files that may contain missing blocks /// /// >A list of remote files IEnumerable GetMissingBlockSources(); } /// /// Implementation of the missing block list /// private class MissingBlockList : IMissingBlockList { /// /// The connection to the database /// private readonly IDbConnection m_connection; /// /// The transaction to use /// private readonly ReusableTransaction m_transaction; /// /// The insert command to use for restoring blocks /// private readonly IDbCommand m_insertCommand; /// /// The command to copy blocks into the duplicate block table /// private readonly IDbCommand m_copyIntoDuplicatedBlocks; /// /// The command to assign blocks to a new volume /// private readonly IDbCommand m_assignBlocksToNewVolume; /// /// The name of the temporary table /// private readonly string m_tablename; /// /// The name of the volume where blocks are missing /// private readonly string m_volumename; /// /// The query to use for getting missing blocks /// private readonly string m_missingBlocksQuery; /// /// Whether the object has been disposed /// private bool m_isDisposed = false; /// /// Creates a new missing block list /// /// The name of the volume with missing blocks /// The connection to the database /// The transaction to use public MissingBlockList(string volumename, IDbConnection connection, ReusableTransaction transaction) { m_connection = connection; m_transaction = transaction; m_volumename = volumename; var tablename = "MissingBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); using (var cmd = m_connection.CreateCommand(m_transaction.Transaction)) { cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{tablename}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL, ""Restored"" INTEGER NOT NULL) ")); m_tablename = tablename; var blockCount = cmd.SetCommandAndParameters(FormatInvariant($@"INSERT INTO ""{m_tablename}"" (""Hash"", ""Size"", ""Restored"") SELECT DISTINCT ""Block"".""Hash"", ""Block"".""Size"", 0 AS ""Restored"" FROM ""Block"",""Remotevolume"" WHERE ""Block"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""Name"" = @Name ")) .SetParameterValue("@Name", volumename) .ExecuteNonQuery(); if (blockCount == 0) throw new Exception($"Unexpected empty block volume: {0}"); cmd.ExecuteNonQuery(FormatInvariant($@"CREATE UNIQUE INDEX ""{tablename}-Ix"" ON ""{tablename}"" (""Hash"", ""Size"", ""Restored"")")); } m_insertCommand = m_connection.CreateCommand(FormatInvariant($@"UPDATE ""{tablename}"" SET ""Restored"" = @NewRestoredValue WHERE ""Hash"" = @Hash AND ""Size"" = @Size AND ""Restored"" = @PreviousRestoredValue ")); m_copyIntoDuplicatedBlocks = m_connection.CreateCommand(@"INSERT OR IGNORE INTO ""DuplicateBlock"" (""BlockID"", ""VolumeID"") SELECT ""Block"".""ID"", ""Block"".""VolumeID"" FROM ""Block"" WHERE ""Block"".""Hash"" = @Hash AND ""Block"".""Size"" = @Size"); m_assignBlocksToNewVolume = m_connection.CreateCommand(@"UPDATE ""Block"" SET ""VolumeID"" = @TargetVolumeId WHERE ""Hash"" = @Hash AND ""Size"" = @Size"); m_missingBlocksQuery = FormatInvariant($@"SELECT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"" FROM ""{m_tablename}"" WHERE ""{m_tablename}"".""Restored"" = @Restored "); } /// public bool SetBlockRestored(string hash, long size, long targetVolumeId) { var restored = m_insertCommand.SetTransaction(m_transaction.Transaction).SetParameterValue("@NewRestoredValue", 1) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .SetParameterValue("@PreviousRestoredValue", 0) .ExecuteNonQuery() == 1; if (restored) { m_copyIntoDuplicatedBlocks.SetTransaction(m_transaction.Transaction) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .ExecuteNonQuery(); var c = m_assignBlocksToNewVolume.SetTransaction(m_transaction.Transaction) .SetParameterValue("@TargetVolumeId", targetVolumeId) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .ExecuteNonQuery(); if (c != 1) throw new Exception($"Unexpected number of updated blocks: {c} != 1"); } return restored; } /// public IEnumerable GetSourceFilesWithBlocks(long blocksize) { using var cmd = m_connection.CreateCommand(m_transaction.Transaction, FormatInvariant($@"SELECT DISTINCT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"", ""BlocksetEntry"".""Index"" * {blocksize} FROM ""{m_tablename}"", ""Block"", ""BlocksetEntry"", ""File"" WHERE ""File"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""Block"".""ID"" = ""BlocksetEntry"".""BlockID"" AND ""{m_tablename}"".""Hash"" = ""Block"".""Hash"" AND ""{m_tablename}"".""Size"" = ""Block"".""Size"" AND ""{m_tablename}"".""Restored"" = @Restored ORDER BY ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"", ""BlocksetEntry"".""Index"" * {blocksize}")) .SetParameterValue("@Restored", 0); foreach (var rd in cmd.ExecuteReaderEnumerable()) { var hash = rd.ConvertValueToString(0) ?? throw new Exception("Hash value was null"); var size = rd.ConvertValueToInt64(1); var file = rd.ConvertValueToString(2) ?? throw new Exception("File value was null"); var offset = rd.ConvertValueToInt64(3); yield return new BlockWithSourceData(hash, size, file, offset); } } /// public IEnumerable GetSourceItemsWithMetadataBlocks() { using var cmd = m_connection.CreateCommand(m_transaction.Transaction, FormatInvariant($@"SELECT DISTINCT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"" FROM ""{m_tablename}"", ""Block"", ""BlocksetEntry"", ""Metadataset"", ""File"" WHERE ""File"".""MetadataID"" == ""Metadataset"".""ID"" AND ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""Block"".""ID"" = ""BlocksetEntry"".""BlockID"" AND ""{m_tablename}"".""Hash"" = ""Block"".""Hash"" AND ""{m_tablename}"".""Size"" = ""Block"".""Size"" AND ""{m_tablename}"".""Restored"" = @Restored ORDER BY ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path""")) .SetParameterValue("@Restored", 0); foreach (var rd in cmd.ExecuteReaderEnumerable()) { var hash = rd.ConvertValueToString(0) ?? throw new Exception("Hash value was null"); var size = rd.ConvertValueToInt64(1); var path = rd.ConvertValueToString(2) ?? throw new Exception("File value was null"); yield return new BlockWithMetadataSourceData(hash, size, path); } } /// public IEnumerable GetBlocklistHashes(long hashesPerBlock) { using var cmd = m_connection.CreateCommand(m_transaction.Transaction); var blocklistTableName = $"BlocklistHashList-{Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray())}"; try { // We need to create a snapshot as we will be updating the m_tablename table during enumeration cmd.SetCommandAndParameters(FormatInvariant($@" CREATE TEMPORARY TABLE ""{blocklistTableName}"" AS SELECT b.""Hash"" AS ""BlockHash"", bs.""Id"" AS ""BlocksetId"", bs.""Length"" AS ""BlocksetLength"", bse.""Index"" / @HashesPerBlock AS ""BlocklistHashIndex"", ( SELECT blh.""Hash"" FROM ""BlocklistHash"" blh WHERE blh.""BlocksetID"" = bs.""ID"" AND blh.""Index"" == bse.""Index"" / @HashesPerBlock LIMIT 1 ) AS ""BlocklistHashHash"", bse.""Index"" AS ""BlocksetEntryIndex"" FROM ""BlocksetEntry"" bse JOIN ""Block"" b ON b.""ID"" = bse.""BlockID"" JOIN ""Blockset"" bs ON bs.""ID"" = bse.""BlocksetID"" WHERE EXISTS ( SELECT 1 FROM ""BlocklistHash"" blh JOIN ""{m_tablename}"" mt ON mt.""Hash"" = blh.""Hash"" WHERE blh.""BlocksetID"" = bs.""ID"" AND mt.""Restored"" = @Restored ) ")) .SetParameterValue("@HashesPerBlock", hashesPerBlock) .SetParameterValue("@Restored", 0) .ExecuteNonQuery(); cmd.SetCommandAndParameters(FormatInvariant($@" SELECT ""BlockHash"", ""BlocksetId"", ""BlocksetLength"", ""BlocklistHashIndex"", ""BlocklistHashHash"", ""BlocksetEntryIndex"" FROM ""{blocklistTableName}"" ORDER BY ""BlocksetId"", ""BlocklistHashIndex"", ""BlocksetEntryIndex""")); foreach (var rd in cmd.ExecuteReaderEnumerable()) { var hash = rd.ConvertValueToString(0) ?? throw new Exception("Block.Hash is null"); var blocksetId = rd.ConvertValueToInt64(1); var length = rd.ConvertValueToInt64(2); var blocklistHashIndex = rd.ConvertValueToInt64(3); var blocklistHash = rd.ConvertValueToString(4) ?? throw new Exception("BlocklistHash is null"); var index = rd.ConvertValueToInt64(5); yield return new BlocklistHashesEntry(blocksetId, blocklistHash, length, blocklistHashIndex, (int)index, hash); } } finally { try { cmd.ExecuteNonQuery(FormatInvariant($@"DROP TABLE IF EXISTS ""{blocklistTableName}"" ")); } catch { } } } /// public long GetMissingBlockCount() { using var cmd = m_connection.CreateCommand(m_transaction.Transaction, FormatInvariant($@"SELECT COUNT(*) FROM ({m_missingBlocksQuery})")) .SetParameterValue("@Restored", 0); return cmd.ExecuteScalarInt64(0); } /// /// Gets the list of missing blocks /// /// >A list of missing blocks public IEnumerable<(string Hash, long Size)> GetMissingBlocks() { using var cmd = m_connection.CreateCommand(m_transaction.Transaction, m_missingBlocksQuery) .SetParameterValue("@Restored", 0); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return (rd.ConvertValueToString(0) ?? "", rd.ConvertValueToInt64(1)); } /// public long MoveBlocksToNewVolume(long targetVolumeId, long sourceVolumeId, IDbTransaction? transaction = null) { if (targetVolumeId <= 0) throw new ArgumentOutOfRangeException(nameof(targetVolumeId), "Target volume ID must be greater than 0"); // Move the source blocks into the DuplicateBlock table using var cmd = m_connection.CreateCommand(transaction, FormatInvariant($@" INSERT OR IGNORE INTO ""DuplicateBlock"" (""BlockID"", ""VolumeID"") SELECT b.""ID"", b.""VolumeID"" FROM ""Block"" b WHERE b.""VolumeID"" = @SourceVolumeId AND (b.""Hash"", b.""Size"") IN ( SELECT ""Hash"", ""Size"" FROM ""{m_tablename}"" WHERE ""Restored"" = @Restored ) ")) .SetParameterValue("@SourceVolumeId", sourceVolumeId) .SetParameterValue("@Restored", 1); var moved = cmd.ExecuteNonQuery(); // Then update the blocks table to point to the new volume cmd.SetCommandAndParameters(FormatInvariant($@" UPDATE ""Block"" SET ""VolumeID"" = @TargetVolumeId WHERE ""VolumeID"" = @SourceVolumeId AND (""Hash"", ""Size"") IN ( SELECT ""Hash"", ""Size"" FROM ""{m_tablename}"" WHERE ""Restored"" = @Restored ) ")) .SetParameterValue("@TargetVolumeId", targetVolumeId) .SetParameterValue("@SourceVolumeId", sourceVolumeId) .SetParameterValue("@Restored", 1); var updated = cmd.ExecuteNonQuery(); if (updated != moved) throw new Exception($"Unexpected number of updated blocks: {updated} != {moved}"); return updated; } public IEnumerable GetFilesetsUsingMissingBlocks() { var blocks = FormatInvariant($@"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{m_tablename}"", ""Block"", ""Blockset"", ""BlocksetEntry"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""BlocksetEntry"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" "); var blocklists = FormatInvariant($@"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{m_tablename}"", ""Block"", ""Blockset"", ""BlocklistHash"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""BlocklistHash"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" "); var cmdtxt = FormatInvariant($@"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"" FROM ""RemoteVolume"", ""FilesetEntry"", ""Fileset"" WHERE ""RemoteVolume"".""ID"" = ""Fileset"".""VolumeID"" AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID"" AND ""RemoteVolume"".""Type"" = @Type AND ""FilesetEntry"".""FileID"" IN (SELECT DISTINCT ""ID"" FROM ( {blocks} UNION {blocklists} ))"); using var cmd = m_connection.CreateCommand(m_transaction.Transaction, cmdtxt) .SetParameterValue("@Type", RemoteVolumeType.Files.ToString()); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return new RemoteVolume(rd.ConvertValueToString(0) ?? "", rd.ConvertValueToString(1) ?? "", rd.ConvertValueToInt64(2)); } public IEnumerable GetMissingBlockSources() { using var cmd = m_connection.CreateCommand(m_transaction.Transaction, FormatInvariant($@"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"" FROM ""RemoteVolume"", ""Block"", ""{m_tablename}"" WHERE ""{m_tablename}"".""Restored"" = @Restored AND ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""Block"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""Remotevolume"".""Name"" != @Name ")) .SetParameterValue("@Restored", 0) .SetParameterValue("@Name", m_volumename); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return new RemoteVolume(rd.ConvertValueToString(0) ?? "", rd.ConvertValueToString(1) ?? "", rd.ConvertValueToInt64(2)); } public void Dispose() { if (m_isDisposed) return; m_isDisposed = true; try { if (m_tablename != null) using (var cmd = m_connection.CreateCommand(m_transaction.Transaction)) cmd.SetTransaction(m_transaction.Transaction).ExecuteNonQuery(FormatInvariant($@"DROP TABLE IF EXISTS ""{m_tablename}"" ")); } catch { } try { m_insertCommand?.Dispose(); } catch { } } } public IMissingBlockList CreateBlockList(string volumename, ReusableTransaction transaction) { return new MissingBlockList(volumename, m_connection, transaction); } public void FixDuplicateMetahash() { using (var tr = m_connection.BeginTransactionSafe()) using (var cmd = m_connection.CreateCommand(tr)) { var sql_count = @"SELECT COUNT(*) FROM (" + @" SELECT DISTINCT c1 FROM (" + @"SELECT COUNT(*) AS ""C1"" FROM (SELECT DISTINCT ""BlocksetID"" FROM ""Metadataset"") UNION SELECT COUNT(*) AS ""C1"" FROM ""Metadataset"" " + @")" + @")"; var x = cmd.ExecuteScalarInt64(sql_count, 0); if (x > 1) { Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateMetadataHashes", "Found duplicate metadatahashes, repairing"); var tablename = "TmpFile-" + Guid.NewGuid().ToString("N"); cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{tablename}"" AS SELECT * FROM ""File""")); var sql = @"SELECT ""A"".""ID"", ""B"".""BlocksetID"" FROM (SELECT MIN(""ID"") AS ""ID"", COUNT(""ID"") AS ""Duplicates"" FROM ""Metadataset"" GROUP BY ""BlocksetID"") ""A"", ""Metadataset"" ""B"" WHERE ""A"".""Duplicates"" > 1 AND ""A"".""ID"" = ""B"".""ID"""; using (var c2 = m_connection.CreateCommand(tr)) { c2.SetCommandAndParameters(FormatInvariant($@"UPDATE ""{tablename}"" SET ""MetadataID"" = @MetadataId WHERE ""MetadataID"" IN (SELECT ""ID"" FROM ""Metadataset"" WHERE ""BlocksetID"" = @BlocksetId)") + @"; DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" = @BlocksetId AND ""ID"" != @MetadataId"); using (var rd = cmd.ExecuteReader(sql)) while (rd.Read()) c2.SetParameterValue("@MetadataId", rd.GetValue(0)) .SetParameterValue("@BlocksetId", rd.GetValue(1)) .ExecuteNonQuery(); } sql = FormatInvariant($@"SELECT ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM ( SELECT MIN(""ID"") AS ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Entries"" FROM ""{tablename}"" GROUP BY ""Path"", ""BlocksetID"", ""MetadataID"") WHERE ""Entries"" > 1 ORDER BY ""ID"""); using (var c2 = m_connection.CreateCommand(tr)) { c2.SetCommandAndParameters(FormatInvariant($@"UPDATE ""FilesetEntry"" SET ""FileID"" = @FileId WHERE ""FileID"" IN (SELECT ""ID"" FROM ""{tablename}"" WHERE ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId)") + FormatInvariant($@"; DELETE FROM ""{tablename}"" WHERE ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId AND ""ID"" != @FileId")); foreach (var rd in cmd.ExecuteReaderEnumerable(sql)) c2.SetParameterValue("@FileId", rd.GetValue(0)) .SetParameterValue("@Path", rd.GetValue(1)) .SetParameterValue("@BlocksetId", rd.GetValue(2)) .SetParameterValue("@MetadataId", rd.GetValue(3)) .ExecuteNonQuery(); } cmd.ExecuteNonQuery(FormatInvariant($@"DELETE FROM ""FileLookup"" WHERE ""ID"" NOT IN (SELECT ""ID"" FROM ""{tablename}"") ")); cmd.ExecuteNonQuery(FormatInvariant($@"CREATE INDEX ""{tablename}-Ix"" ON ""{tablename}"" (""ID"", ""MetadataID"")")); cmd.ExecuteNonQuery(FormatInvariant($@"UPDATE ""FileLookup"" SET ""MetadataID"" = (SELECT ""MetadataID"" FROM ""{tablename}"" A WHERE ""A"".""ID"" = ""FileLookup"".""ID"") ")); cmd.ExecuteNonQuery(FormatInvariant($@"DROP TABLE ""{tablename}"" ")); x = cmd.ExecuteScalarInt64(sql_count, 0); if (x > 1) throw new Interface.UserInformationException("Repair failed, there are still duplicate metadatahashes!", "DuplicateHashesRepairFailed"); Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateMetadataHashesFixed", "Duplicate metadatahashes repaired succesfully"); tr.Commit(); } } } public void FixDuplicateFileentries() { using (var tr = m_connection.BeginTransactionSafe()) using (var cmd = m_connection.CreateCommand(tr)) { var sql_count = @"SELECT COUNT(*) FROM (SELECT ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Duplicates"" FROM ""FileLookup"" GROUP BY ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"") WHERE ""Duplicates"" > 1"; var x = cmd.ExecuteScalarInt64(sql_count, 0); if (x > 0) { Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateFileEntries", "Found duplicate file entries, repairing"); var sql = @"SELECT ""ID"", ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM ( SELECT MIN(""ID"") AS ""ID"", ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Entries"" FROM ""FileLookup"" GROUP BY ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"") WHERE ""Entries"" > 1 ORDER BY ""ID"""; using (var c2 = m_connection.CreateCommand(tr)) { c2.SetCommandAndParameters(@"UPDATE ""FilesetEntry"" SET ""FileID"" = @FileId WHERE ""FileID"" IN (SELECT ""ID"" FROM ""FileLookup"" WHERE ""PrefixID"" = @PrefixId AND ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetatadataId)" + @"; DELETE FROM ""FileLookup"" WHERE ""PrefixID"" = @PrefixId AND ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId AND ""ID"" != @FileId"); foreach (var rd in cmd.SetCommandAndParameters(sql).ExecuteReaderEnumerable()) c2.SetParameterValue("@FileId", rd.GetValue(0)) .SetParameterValue("@PrefixId", rd.GetValue(1)) .SetParameterValue("@Path", rd.GetValue(2)) .SetParameterValue("@BlocksetId", rd.GetValue(3)) .SetParameterValue("@MetadataId", rd.GetValue(4)) .ExecuteNonQuery(); } x = cmd.ExecuteScalarInt64(sql_count, 0); if (x > 1) throw new Interface.UserInformationException("Repair failed, there are still duplicate file entries!", "DuplicateFilesRepairFailed"); Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateFileEntriesFixed", "Duplicate file entries repaired succesfully"); tr.Commit(); } } } public void FixMissingBlocklistHashes(string blockhashalgorithm, long blocksize) { var blocklistbuffer = new byte[blocksize]; using (var tr = m_connection.BeginTransactionSafe()) using (var cmd = m_connection.CreateCommand(tr)) using (var blockhasher = HashFactory.CreateHasher(blockhashalgorithm)) { var hashsize = blockhasher.HashSize / 8; var sql = FormatInvariant($@"SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {blocksize / hashsize} - 1) / {blocksize / hashsize}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual"""); var countsql = @"SELECT COUNT(*) FROM (" + sql + @")"; var itemswithnoblocklisthash = cmd.ExecuteScalarInt64(countsql, 0); if (itemswithnoblocklisthash != 0) { Logging.Log.WriteInformationMessage(LOGTAG, "MissingBlocklistHashes", "Found {0} missing blocklisthash entries, repairing", itemswithnoblocklisthash); using (var c2 = m_connection.CreateCommand(tr)) using (var c3 = m_connection.CreateCommand(tr)) using (var c4 = m_connection.CreateCommand(tr)) using (var c5 = m_connection.CreateCommand(tr)) using (var c6 = m_connection.CreateCommand(tr)) { c3.SetCommandAndParameters(@"INSERT INTO ""BlocklistHash"" (""BlocksetID"", ""Index"", ""Hash"") VALUES (@BlocksetId, @Index, @Hash) "); c4.SetCommandAndParameters(@"SELECT ""ID"" FROM ""Block"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size"); c5.SetCommandAndParameters(@"SELECT ""ID"" FROM ""DeletedBlock"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size AND ""VolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND (""State"" IN (@State1, @State2)))"); c6.SetCommandAndParameters(@"INSERT INTO ""Block"" (""Hash"", ""Size"", ""VolumeID"") SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""DeletedBlock"" WHERE ""ID"" = @DeletedBlockId LIMIT 1; DELETE FROM ""DeletedBlock"" WHERE ""ID"" = @DeletedBlockId;"); foreach (var e in cmd.ExecuteReaderEnumerable(sql)) { var blocksetid = e.ConvertValueToInt64(0); var ix = 0L; int blocklistoffset = 0; c2.SetCommandAndParameters(@"DELETE FROM ""BlocklistHash"" WHERE ""BlocksetID"" = @BlocksetId") .SetParameterValue("@BlocksetId", blocksetid) .ExecuteNonQuery(); c2.SetCommandAndParameters(@"SELECT ""A"".""Hash"" FROM ""Block"" ""A"", ""BlocksetEntry"" ""B"" WHERE ""A"".""ID"" = ""B"".""BlockID"" AND ""B"".""BlocksetID"" = @BlocksetId ORDER BY ""B"".""Index""") .SetParameterValue("@BlocksetId", blocksetid); foreach (var h in c2.ExecuteReaderEnumerable()) { var tmp = Convert.FromBase64String(h.ConvertValueToString(0) ?? throw new Exception("Hash value was null")); if (blocklistbuffer.Length - blocklistoffset < tmp.Length) { var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, blocklistoffset)); // Check if the block exists in "blocks" var existingBlockId = c4.SetParameterValue("@Hash", blkey) .SetParameterValue("@Size", blocklistoffset) .ExecuteScalarInt64(-1); if (existingBlockId <= 0) { var deletedBlockId = c5.SetParameterValue("@Hash", blkey) .SetParameterValue("@Size", blocklistoffset) .SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString()) .SetParameterValue("@State1", RemoteVolumeState.Uploaded.ToString()) .SetParameterValue("@State2", RemoteVolumeState.Verified.ToString()) .ExecuteScalarInt64(-1); if (deletedBlockId <= 0) throw new Exception($"Missing block for blocklisthash: {blkey}"); else { var rc = c6.SetParameterValue("@DeletedBlockId", deletedBlockId) .ExecuteNonQuery(); if (rc != 2) throw new Exception($"Unexpected update count: {rc}"); } } // Add to table c3.SetParameterValue("@BlocksetId", blocksetid) .SetParameterValue("@Index", ix) .SetParameterValue("@Hash", blkey) .ExecuteNonQuery(); ix++; blocklistoffset = 0; } Array.Copy(tmp, 0, blocklistbuffer, blocklistoffset, tmp.Length); blocklistoffset += tmp.Length; } if (blocklistoffset != 0) { var blkeyfinal = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, blocklistoffset)); // Ensure that the block exists in "blocks" var existingBlockId = c4.SetParameterValue("@Hash", blkeyfinal) .SetParameterValue("@Size", blocklistoffset) .ExecuteScalarInt64(-1); if (existingBlockId <= 0) { var deletedBlockId = c5.SetParameterValue("@Hash", blkeyfinal) .SetParameterValue("@Size", blocklistoffset) .SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString()) .SetParameterValue("@State1", RemoteVolumeState.Uploaded.ToString()) .SetParameterValue("@State2", RemoteVolumeState.Verified.ToString()) .ExecuteScalarInt64(-1); if (deletedBlockId == 0) throw new Exception($"Missing block for blocklisthash: {blkeyfinal}"); else { var rc = c6.SetParameterValue("@DeletedBlockId", deletedBlockId) .ExecuteNonQuery(); if (rc != 2) throw new Exception($"Unexpected update count: {rc}"); } } // Add to table c3.SetParameterValue("@BlocksetId", blocksetid) .SetParameterValue("@Index", ix) .SetParameterValue("@Hash", blkeyfinal) .ExecuteNonQuery(); } } } itemswithnoblocklisthash = cmd.ExecuteScalarInt64(countsql, 0); if (itemswithnoblocklisthash != 0) throw new Interface.UserInformationException($"Failed to repair, after repair {itemswithnoblocklisthash} blocklisthashes were missing", "MissingBlocklistHashesRepairFailed"); Logging.Log.WriteInformationMessage(LOGTAG, "MissingBlocklisthashesRepaired", "Missing blocklisthashes repaired succesfully"); tr.Commit(); } } } public void FixDuplicateBlocklistHashes(long blocksize, long hashsize) { using (var tr = m_connection.BeginTransactionSafe()) using (var cmd = m_connection.CreateCommand(tr)) { var dup_sql = @"SELECT * FROM (SELECT ""BlocksetID"", ""Index"", COUNT(*) AS ""EC"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"", ""Index"") WHERE ""EC"" > 1"; var sql_count = @"SELECT COUNT(*) FROM (" + dup_sql + ")"; var x = cmd.ExecuteScalarInt64(sql_count, 0); if (x > 0) { Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateBlocklistHashes", "Found duplicate blocklisthash entries, repairing"); var unique_count = cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")", 0); using (var c2 = m_connection.CreateCommand(tr)) { c2.SetCommandAndParameters(@"DELETE FROM ""BlocklistHash"" WHERE rowid IN (SELECT rowid FROM ""BlocklistHash"" WHERE ""BlocksetID"" = @BlocksetId AND ""Index"" = @Index LIMIT @Limit)"); foreach (var rd in cmd.ExecuteReaderEnumerable(dup_sql)) { var expected = rd.GetInt32(2) - 1; var actual = c2.SetParameterValue("@BlocksetId", rd.GetValue(0)) .SetParameterValue("@Index", rd.GetValue(1)) .SetParameterValue("@Limit", expected) .ExecuteNonQuery(); if (actual != expected) throw new Exception($"Unexpected number of results after fix, got: {actual}, expected: {expected}"); } } x = cmd.ExecuteScalarInt64(sql_count); if (x > 1) throw new Exception("Repair failed, there are still duplicate file entries!"); var real_count = cmd.ExecuteScalarInt64(@"SELECT Count(*) FROM ""BlocklistHash""", 0); if (real_count != unique_count) throw new Interface.UserInformationException($"Failed to repair, result should have been {unique_count} blocklist hashes, but result was {real_count} blocklist hashes", "DuplicateBlocklistHashesRepairFailed"); try { VerifyConsistency(blocksize, hashsize, true, tr); } catch (Exception ex) { throw new Interface.UserInformationException("Repaired blocklisthashes, but the database was broken afterwards, rolled back changes", "DuplicateBlocklistHashesRepairFailed", ex); } Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateBlocklistHashesRepaired", "Duplicate blocklisthashes repaired succesfully"); tr.Commit(); } } } public void CheckAllBlocksAreInVolume(string filename, IEnumerable> blocks, IDbTransaction transaction) { using var cmd = m_connection.CreateCommand(transaction); var tablename = "ProbeBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); try { cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{tablename}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL)")); cmd.SetCommandAndParameters(FormatInvariant($@"INSERT INTO ""{tablename}"" (""Hash"", ""Size"") VALUES (@Hash, @Size)")); foreach (var kp in blocks) { cmd.SetParameterValue("@Hash", kp.Key); cmd.SetParameterValue("@Size", kp.Value); cmd.ExecuteNonQuery(); } var id = cmd.SetCommandAndParameters(@"SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = @Name") .SetParameterValue("@Name", filename) .ExecuteScalarInt64(-1); var aliens = cmd.SetCommandAndParameters(FormatInvariant($@"SELECT COUNT(*) FROM (SELECT ""A"".""VolumeID"" FROM ""{tablename}"" B LEFT OUTER JOIN ""Block"" A ON ""A"".""Hash"" = ""B"".""Hash"" AND ""A"".""Size"" = ""B"".""Size"") WHERE ""VolumeID"" != @VolumeId ")) .SetParameterValue("@VolumeId", id) .ExecuteScalarInt64(0); if (aliens != 0) throw new Exception($"Not all blocks were found in {filename}"); } finally { cmd.ExecuteNonQuery(FormatInvariant($@"DROP TABLE IF EXISTS ""{tablename}"" ")); } } public void CheckBlocklistCorrect(string hash, long length, IEnumerable blocklist, long blocksize, long blockhashlength, IDbTransaction? transaction) { using (var cmd = m_connection.CreateCommand(transaction)) { var query = FormatInvariant($@" SELECT ""C"".""Hash"", ""C"".""Size"" FROM ""BlocksetEntry"" A, ( SELECT ""Y"".""BlocksetID"", ""Y"".""Hash"" AS ""BlocklistHash"", ""Y"".""Index"" AS ""BlocklistHashIndex"", ""Z"".""Size"" AS ""BlocklistSize"", ""Z"".""ID"" AS ""BlocklistHashBlockID"" FROM ""BlocklistHash"" Y, ""Block"" Z WHERE ""Y"".""Hash"" = ""Z"".""Hash"" AND ""Y"".""Hash"" = @Hash AND ""Z"".""Size"" = @Size LIMIT 1 ) B, ""Block"" C WHERE ""A"".""BlocksetID"" = ""B"".""BlocksetID"" AND ""A"".""BlockID"" = ""C"".""ID"" AND ""A"".""Index"" >= ""B"".""BlocklistHashIndex"" * ({blocksize} / {blockhashlength}) AND ""A"".""Index"" < (""B"".""BlocklistHashIndex"" + 1) * ({blocksize} / {blockhashlength}) ORDER BY ""A"".""Index"" "); using (var en = blocklist.GetEnumerator()) { cmd.SetCommandAndParameters(query) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", length); foreach (var r in cmd.ExecuteReaderEnumerable()) { if (!en.MoveNext()) throw new Exception($"Too few entries in source blocklist with hash {hash}"); if (en.Current != r.ConvertValueToString(0)) throw new Exception($"Mismatch in blocklist with hash {hash}"); } if (en.MoveNext()) throw new Exception($"Too many source blocklist entries in {hash}"); } } } public IEnumerable MissingLocalFilesets() { using var cmd = m_connection.CreateCommand(@"SELECT ""Name"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" NOT IN (@States) AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")") .SetParameterValue("@Type", RemoteVolumeType.Files.ToString()) .ExpandInClauseParameter("@States", [RemoteVolumeState.Deleting.ToString(), RemoteVolumeState.Deleted.ToString()]); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return rd.ConvertValueToString(0) ?? ""; } public IEnumerable<(long FilesetID, DateTime Timestamp, bool IsFull)> MissingRemoteFilesets() { using var cmd = m_connection.CreateCommand(@"SELECT ""ID"", ""Timestamp"", ""IsFullBackup"" FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" NOT IN (@States))") .SetParameterValue("@Type", RemoteVolumeType.Files.ToString()) .ExpandInClauseParameter("@States", [RemoteVolumeState.Deleting.ToString(), RemoteVolumeState.Deleted.ToString()]); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return (rd.ConvertValueToInt64(0), ParseFromEpochSeconds(rd.ConvertValueToInt64(1)), rd.ConvertValueToInt64(2) == BackupType.FULL_BACKUP); } public IEnumerable EmptyIndexFiles() { using var cmd = m_connection.CreateCommand(@"SELECT ""Name"", ""Hash"", ""Size"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" IN (@States) AND ""ID"" NOT IN (SELECT ""IndexVolumeId"" FROM ""IndexBlockLink"")") .SetParameterValue("@Type", RemoteVolumeType.Index.ToString()) .ExpandInClauseParameter("@States", [RemoteVolumeState.Uploaded.ToString(), RemoteVolumeState.Verified.ToString()]); foreach (var rd in cmd.ExecuteReaderEnumerable()) yield return new RemoteVolume(rd.ConvertValueToString(0) ?? "", rd.ConvertValueToString(1) ?? "", rd.ConvertValueToInt64(2)); } public void FixEmptyMetadatasets(Options options) { using var tr = m_connection.BeginTransactionSafe(); using var cmd = m_connection.CreateCommand(tr, @"SELECT COUNT(*) FROM Metadataset JOIN Blockset ON Metadataset.BlocksetID = Blockset.ID WHERE Blockset.Length = 0"); var emptyMetaCount = cmd.ExecuteScalarInt64(0); if (emptyMetaCount <= 0) return; Logging.Log.WriteInformationMessage(LOGTAG, "ZeroLengthMetadata", "Found {0} zero-length metadata entries", emptyMetaCount); // Create replacement metadata var emptyMeta = Utility.WrapMetadata(new Dictionary(), options); var emptyBlocksetId = GetEmptyMetadataBlocksetId(Array.Empty(), emptyMeta.FileHash, emptyMeta.Blob.Length, tr); if (emptyBlocksetId < 0) throw new Interface.UserInformationException( "Failed to locate an empty metadata blockset to replace missing metadata. Set the option --disable-replace-missing-metadata=true to ignore this and drop files with missing metadata.", "FailedToLocateEmptyMetadataBlockset"); // Step 1: Create temp table with Metadataset IDs referencing empty blocksets (excluding the one to keep) var tablename = "FixMetadatasets-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); try { cmd.SetCommandAndParameters( @$"CREATE TEMP TABLE ""{tablename}"" AS SELECT m.ID AS MetadataID, m.BlocksetID FROM Metadataset m JOIN Blockset b ON m.BlocksetID = b.ID WHERE b.Length = 0 AND m.BlocksetID != @KeepBlockset") .SetParameterValue("@KeepBlockset", emptyBlocksetId) .ExecuteNonQuery(); // Step 2: Update FileLookup to use a valid metadata ID cmd.SetCommandAndParameters( @$"UPDATE FileLookup SET MetadataID = ( SELECT ID FROM Metadataset WHERE BlocksetID = @KeepBlockset LIMIT 1 ) WHERE MetadataID IN (SELECT MetadataID FROM ""{tablename}"")") .SetParameterValue("@KeepBlockset", emptyBlocksetId) .ExecuteNonQuery(); // Step 3: Delete obsolete Metadataset entries cmd.SetCommandAndParameters( @$"DELETE FROM Metadataset WHERE ID IN (SELECT MetadataID FROM ""{tablename}"")") .ExecuteNonQuery(); // Step 4: Delete orphaned blocksets (affected only) cmd.SetCommandAndParameters( @$"DELETE FROM Blockset WHERE ID IN (SELECT BlocksetID FROM ""{tablename}"") AND NOT EXISTS (SELECT 1 FROM Metadataset WHERE BlocksetID = Blockset.ID) AND NOT EXISTS (SELECT 1 FROM File WHERE BlocksetID = Blockset.ID)") .ExecuteNonQuery(); // Step 5: Confirm all broken metadata entries are resolved cmd.SetCommandAndParameters( @"SELECT COUNT(*) FROM Metadataset JOIN Blockset ON Metadataset.BlocksetID = Blockset.ID WHERE Blockset.Length = 0"); var remaining = cmd.ExecuteScalarInt64(0); if (remaining > 0) throw new Interface.UserInformationException( "Some zero-length metadata entries could not be repaired.", "MetadataRepairFailed"); Logging.Log.WriteInformationMessage(LOGTAG, "ZeroLengthMetadataRepaired", "Zero length metadata entries repaired successfully"); tr.Commit(); } finally { try { cmd.SetCommandAndParameters(FormatInvariant($@"DROP TABLE IF EXISTS ""{tablename}"" ")) .ExecuteNonQuery(); } catch (Exception ex) { Logging.Log.WriteVerboseMessage(LOGTAG, "ErrorDroppingTempTable", ex, "Failed to drop temporary table {0}: {1}", tablename, ex.Message); } } } } }