mirror of
https://github.com/duplicati/duplicati.git
synced 2026-05-06 07:16:38 -04:00
4244e71fee
This PR improves SQL logging by capturing many places where the calls would previously just hit the database and not produce any logging output.
2108 lines
95 KiB
C#
2108 lines
95 KiB
C#
// Copyright (C) 2026, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Utility;
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
namespace Duplicati.Library.Main.Database
|
|
{
|
|
/// <summary>
|
|
/// Represents a specialized local database used for repair operations in Duplicati.
|
|
/// Provides methods for creating and managing repair databases, handling missing or duplicate blocks,
|
|
/// repairing metadata and file entries, and verifying consistency of backup data.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This class extends <see cref="LocalDatabase"/> and provides additional functionality
|
|
/// for repairing and maintaining the integrity of backup databases, including block and fileset management,
|
|
/// duplicate detection and correction, and blocklist hash repairs.
|
|
/// </remarks>
|
|
internal class LocalRepairDatabase : LocalDatabase
|
|
{
|
|
/// <summary>
|
|
/// The tag used for logging.
|
|
/// </summary>
|
|
private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalRepairDatabase));
|
|
|
|
/// <summary>
|
|
/// Creates a new local repair database.
|
|
/// </summary>
|
|
/// <param name="path">The path to the database.</param>
|
|
/// <param name="dbnew">An optional existing database instance to use.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns> A task that when awaited returns a new instance of <see cref="LocalRepairDatabase"/>.</returns>
|
|
public static async Task<LocalRepairDatabase> CreateRepairDatabase(string path, LocalRepairDatabase? dbnew, CancellationToken token)
|
|
{
|
|
dbnew ??= new LocalRepairDatabase();
|
|
|
|
dbnew = (LocalRepairDatabase)
|
|
await CreateLocalDatabaseAsync(path, "Repair", true, dbnew, token)
|
|
.ConfigureAwait(false);
|
|
|
|
return dbnew;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new local repair database from an existing local database.
|
|
/// </summary>
|
|
/// <param name="dbparent">The parent local database to use.</param>
|
|
/// <param name="dbnew">An optional existing database instance to use.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns a new instance of <see cref="LocalRepairDatabase"/>.</returns>
|
|
public static async Task<LocalRepairDatabase> CreateAsync(LocalDatabase dbparent, LocalRepairDatabase? dbnew, CancellationToken token)
|
|
{
|
|
dbnew ??= new LocalRepairDatabase();
|
|
|
|
return (LocalRepairDatabase)
|
|
await CreateLocalDatabaseAsync(dbparent, dbnew, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the fileset ID from the remote name.
|
|
/// </summary>
|
|
/// <param name="filelist">The remote name of the fileset.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns a tuple containing the fileset ID, timestamp, and whether it is a full backup.</returns>
|
|
/// <exception cref="Exception">Thrown if the remote file does not exist.</exception>
|
|
public async Task<(long FilesetId, DateTime Time, bool IsFullBackup)> GetFilesetFromRemotename(string filelist, CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(@"
|
|
SELECT
|
|
""Fileset"".""ID"",
|
|
""Fileset"".""Timestamp"",
|
|
""Fileset"".""IsFullBackup""
|
|
FROM
|
|
""Fileset"",
|
|
""RemoteVolume""
|
|
WHERE
|
|
""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID""
|
|
AND ""RemoteVolume"".""Name"" = @Name
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Name", filelist);
|
|
|
|
var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false);
|
|
|
|
if (!await rd.ReadAsync(token).ConfigureAwait(false))
|
|
throw new Exception($"No such remote file: {filelist}");
|
|
|
|
return (
|
|
rd.ConvertValueToInt64(0, -1),
|
|
ParseFromEpochSeconds(rd.ConvertValueToInt64(1)).ToLocalTime(),
|
|
rd.GetInt32(2) == BackupType.FULL_BACKUP
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves entries in the FilesetEntry table from previous fileset to current fileset.
|
|
/// </summary>
|
|
/// <param name="filesetid">Current fileset ID.</param>
|
|
/// <param name="prevFilesetId">Source fileset ID.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
/// <exception cref="ArgumentException">Thrown if either fileset ID is less than or equal to zero.</exception>
|
|
public async Task MoveFilesFromFileset(long filesetid, long prevFilesetId, CancellationToken token)
|
|
{
|
|
if (filesetid <= 0)
|
|
throw new ArgumentException("filesetid must be > 0");
|
|
|
|
if (prevFilesetId <= 0)
|
|
throw new ArgumentException("prevId must be > 0");
|
|
|
|
await using var cmd = m_connection.CreateCommand(@"
|
|
UPDATE ""FilesetEntry""
|
|
SET ""FilesetID"" = @CurrentFilesetId
|
|
WHERE ""FilesetID"" = @PreviousFilesetId
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@CurrentFilesetId", filesetid)
|
|
.SetParameterValue("@PreviousFilesetId", prevFilesetId);
|
|
|
|
await cmd.ExecuteNonQueryAsync(true, token).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the list of index files that reference a given block file.
|
|
/// </summary>
|
|
/// <param name="blockfileid">The block file ID.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of index file names that reference the specified block file.</returns>
|
|
public async IAsyncEnumerable<string> GetIndexFilesReferencingBlockFile(long blockfileid, [EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(@"
|
|
SELECT ""RemoteVolume"".""Name""
|
|
FROM
|
|
""RemoteVolume"",
|
|
""IndexBlockLink""
|
|
WHERE
|
|
""IndexBlockLink"".""BlockVolumeID"" = @BlockFileId
|
|
AND ""RemoteVolume"".""ID"" = ""IndexBlockLink"".""IndexVolumeID""
|
|
AND ""RemoteVolume"".""Type"" = @Type
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@BlockFileId", blockfileid)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Index.ToString());
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return rd.ConvertValueToString(0) ?? throw new Exception("RemoteVolume name was null");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of filesets that are missing files.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of key-value pairs where the key is the fileset ID and the value is the timestamp of the fileset.</returns>
|
|
public async IAsyncEnumerable<KeyValuePair<long, DateTime>> GetFilesetsWithMissingFiles([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(@"
|
|
SELECT
|
|
""ID"",
|
|
""Timestamp""
|
|
FROM ""Fileset""
|
|
WHERE ""ID"" IN (
|
|
SELECT ""FilesetID""
|
|
FROM ""FilesetEntry""
|
|
WHERE ""FileID"" NOT IN (
|
|
SELECT ""ID""
|
|
FROM ""FileLookup""
|
|
)
|
|
)
|
|
")
|
|
.SetTransaction(m_rtr);
|
|
|
|
await using var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false);
|
|
while (await rd.ReadAsync(token).ConfigureAwait(false))
|
|
{
|
|
yield return new KeyValuePair<long, DateTime>(
|
|
rd.ConvertValueToInt64(0),
|
|
ParseFromEpochSeconds(rd.ConvertValueToInt64(1)).ToLocalTime()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all fileset entries for a given fileset.
|
|
/// </summary>
|
|
/// <param name="filesetid">The fileset ID.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns the number of deleted entries.</returns>
|
|
public async Task<int> DeleteFilesetEntries(long filesetid, CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(@"
|
|
DELETE FROM ""FilesetEntry""
|
|
WHERE ""FilesetID"" = @FilesetId
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@FilesetId", filesetid);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(true, token).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a remote volume with its name, hash, and size.
|
|
/// </summary>
|
|
private class RemoteVolume : IRemoteVolume
|
|
{
|
|
/// <summary>
|
|
/// The name of the remote volume.
|
|
/// </summary>
|
|
public string Name { get; private set; }
|
|
/// <summary>
|
|
/// The hash of the remote volume.
|
|
/// </summary>
|
|
public string Hash { get; private set; }
|
|
/// <summary>
|
|
/// The size of the remote volume in bytes.
|
|
/// </summary>
|
|
public long Size { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="RemoteVolume"/> class.
|
|
/// </summary>
|
|
/// <param name="name">The name of the remote volume.</param>
|
|
/// <param name="hash">The hash of the remote volume.</param>
|
|
/// <param name="size">The size of the remote volume in bytes.</param>
|
|
public RemoteVolume(string name, string hash, long size)
|
|
{
|
|
this.Name = name;
|
|
this.Hash = hash;
|
|
this.Size = size;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of block volumes that are associated with a given index name.
|
|
/// </summary>
|
|
/// <param name="indexName">The name of the index volume.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="IRemoteVolume"/> representing the block volumes.</returns>
|
|
public async IAsyncEnumerable<IRemoteVolume> GetBlockVolumesFromIndexName(string indexName, [EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await 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
|
|
)
|
|
)
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Name", indexName);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return new RemoteVolume(
|
|
rd.ConvertValueToString(0) ?? "",
|
|
rd.ConvertValueToString(1) ?? "",
|
|
rd.ConvertValueToInt64(2)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A single block with source data for repair.
|
|
/// </summary>
|
|
/// <param name="Hash">The block hash.</param>
|
|
/// <param name="Size">The block size.</param>
|
|
/// <param name="File">The file that contains the block.</param>
|
|
/// <param name="Offset">The offset of the block in the file.</param>
|
|
public sealed record BlockWithSourceData(string Hash, long Size, string File, long Offset);
|
|
|
|
/// <summary>
|
|
/// A single block with metadata source data for repair.
|
|
/// </summary>
|
|
/// <param name="Hash">The block hash.</param>
|
|
/// <param name="Size">The block size.</param>
|
|
/// <param name="Path">The path of the file or directory that contains the block.</param>
|
|
public sealed record BlockWithMetadataSourceData(string Hash, long Size, string Path);
|
|
|
|
/// <summary>
|
|
/// A single blocklist hash entry.
|
|
/// </summary>
|
|
/// <param name="BlocksetId">The blockset id.</param>
|
|
/// <param name="BlocklistHash">The hash of the blocklist entry (when done).</param>
|
|
/// <param name="BlocklistHashLength">The total length of the blockset.</param>
|
|
/// <param name="BlocklistHashIndex">The index of the blocklist hash.</param>
|
|
/// <param name="Index">The index of the block in the blockset.</param>
|
|
/// <param name="Hash">The hash of the entry.</param>
|
|
public sealed record BlocklistHashesEntry(
|
|
long BlocksetId,
|
|
string BlocklistHash,
|
|
long BlocklistHashLength,
|
|
long BlocklistHashIndex,
|
|
int Index,
|
|
string Hash);
|
|
|
|
/// <summary>
|
|
/// Helper interface for the missing block list.
|
|
/// </summary>
|
|
public interface IMissingBlockList : IDisposable, IAsyncDisposable
|
|
{
|
|
/// <summary>
|
|
/// Registers a block as restored.
|
|
/// </summary>
|
|
/// <param name="hash">The block hash.</param>
|
|
/// <param name="size">The block size.</param>
|
|
/// <param name="volumeId">The volume ID of the new target volume.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns true if the block was successfully marked as restored, false otherwise.</returns>
|
|
Task<bool> SetBlockRestored(string hash, long size, long volumeId, CancellationToken token);
|
|
/// <summary>
|
|
/// Gets the list of files that contains missing blocks.
|
|
/// </summary>
|
|
/// <param name="blocksize">The blocksize setting.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="BlockWithSourceData"/> representing the files with missing blocks.</returns>
|
|
IAsyncEnumerable<BlockWithSourceData> GetSourceFilesWithBlocks(long blocksize, CancellationToken token);
|
|
/// <summary>
|
|
/// Gets a list for filesystem entries that contain missing blocks in metadata.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="BlockWithMetadataSourceData"/> representing the metadata blocks.</returns>
|
|
IAsyncEnumerable<BlockWithMetadataSourceData> GetSourceItemsWithMetadataBlocks(CancellationToken token);
|
|
/// <summary>
|
|
/// Gets missing blocklist hashes.
|
|
/// </summary>
|
|
/// <param name="hashesPerBlock">The number of hashes for each block.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="BlocklistHashesEntry"/> representing the blocklist hashes.</returns>
|
|
IAsyncEnumerable<BlocklistHashesEntry> GetBlocklistHashes(long hashesPerBlock, CancellationToken token);
|
|
/// <summary>
|
|
/// Gets the number of missing blocks.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns the count of missing blocks.</returns>
|
|
Task<long> GetMissingBlockCount(CancellationToken token);
|
|
/// <summary>
|
|
/// Gets all the filesets that are affected by missing blocks.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="IRemoteVolume"/> representing the filesets that contain missing blocks.</returns>
|
|
IAsyncEnumerable<IRemoteVolume> GetFilesetsUsingMissingBlocks(CancellationToken token);
|
|
/// <summary>
|
|
/// Gets a list of remote files that may contain missing blocks.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of <see cref="IRemoteVolume"/> representing the remote volumes that may contain missing blocks.</returns>
|
|
IAsyncEnumerable<IRemoteVolume> GetMissingBlockSources(CancellationToken token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementation of the missing block list.
|
|
/// </summary>
|
|
private class MissingBlockList : IMissingBlockList
|
|
{
|
|
/// <summary>
|
|
/// The connection to the database
|
|
/// </summary>
|
|
private readonly SqliteConnection m_connection;
|
|
/// <summary>
|
|
/// The transaction to use
|
|
/// </summary>
|
|
private readonly ReusableTransaction m_rtr;
|
|
/// <summary>
|
|
/// Updates the "Restored" status of a block in the temporary missing blocks table for a given hash and size.
|
|
/// </summary>
|
|
private SqliteCommand m_insertCommand = null!;
|
|
/// <summary>
|
|
/// Inserts or ignores a block and its volume assignment into the "DuplicateBlock" table for a given hash and size.
|
|
/// </summary>
|
|
private SqliteCommand m_copyIntoDuplicatedBlocks = null!;
|
|
/// <summary>
|
|
/// Updates the "VolumeID" of a block in the "Block" table to assign it to a new volume for a given hash and size.
|
|
/// </summary>
|
|
private SqliteCommand m_assignBlocksToNewVolume = null!;
|
|
/// <summary>
|
|
/// Selects the hash and size of all missing blocks (where "Restored" is 0) from the temporary missing blocks table.
|
|
/// </summary>
|
|
private SqliteCommand m_missingBlocksCommand = null!;
|
|
/// <summary>
|
|
/// Counts the number of missing blocks (where "Restored" is 0) in the temporary missing blocks table.
|
|
/// </summary>
|
|
private SqliteCommand m_missingBlocksCountCommand = null!;
|
|
|
|
/// <summary>
|
|
/// The name of the temporary table.
|
|
/// </summary>
|
|
private readonly string m_tablename;
|
|
/// <summary>
|
|
/// The name of the volume where blocks are missing.
|
|
/// </summary>
|
|
private readonly string m_volumename;
|
|
|
|
/// <summary>
|
|
/// Whether the object has been disposed.
|
|
/// </summary>
|
|
private bool m_isDisposed = false;
|
|
|
|
/// <summary>
|
|
/// Creates a new missing block list.
|
|
/// </summary>
|
|
/// <param name="volumename">The name of the volume with missing blocks.</param>
|
|
/// <param name="connection">The connection to the database.</param>
|
|
/// <param name="rtr">The transaction to use.</param>
|
|
private MissingBlockList(string volumename, SqliteConnection connection, ReusableTransaction rtr)
|
|
{
|
|
m_connection = connection;
|
|
m_rtr = rtr;
|
|
m_volumename = volumename;
|
|
var tablename = $"MissingBlocks-{Library.Utility.Utility.GetHexGuid()}";
|
|
m_tablename = tablename;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of the missing block list.
|
|
/// </summary>
|
|
/// <param name="volumename">The name of the volume with missing blocks.</param>
|
|
/// <param name="connection">The connection to the database.</param>
|
|
/// <param name="transaction">The transaction to use.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns a new instance of <see cref="IMissingBlockList"/>.</returns>
|
|
public static async Task<IMissingBlockList> CreateMissingBlockList(string volumename, SqliteConnection connection, ReusableTransaction transaction, CancellationToken token)
|
|
{
|
|
var blocklist = new MissingBlockList(volumename, connection, transaction);
|
|
await using (var cmd = connection.CreateCommand(transaction.Transaction))
|
|
{
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
CREATE TEMPORARY TABLE ""{blocklist.m_tablename}"" (
|
|
""Hash"" TEXT NOT NULL,
|
|
""Size"" INTEGER NOT NULL,
|
|
""Restored"" INTEGER NOT NULL
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
cmd.SetCommandAndParameters($@"
|
|
INSERT INTO ""{blocklist.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);
|
|
|
|
var blockCount = await cmd.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (blockCount == 0)
|
|
throw new Exception($"Unexpected empty block volume: {0}");
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
CREATE UNIQUE INDEX ""{blocklist.m_tablename}-Ix""
|
|
ON ""{blocklist.m_tablename}"" (
|
|
""Hash"",
|
|
""Size"",
|
|
""Restored""
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
blocklist.m_insertCommand = await connection.CreateCommandAsync($@"
|
|
UPDATE ""{blocklist.m_tablename}""
|
|
SET ""Restored"" = @NewRestoredValue
|
|
WHERE
|
|
""Hash"" = @Hash
|
|
AND ""Size"" = @Size
|
|
AND ""Restored"" = @PreviousRestoredValue
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
blocklist.m_copyIntoDuplicatedBlocks = await connection.CreateCommandAsync(@"
|
|
INSERT OR IGNORE INTO ""DuplicateBlock"" (
|
|
""BlockID"",
|
|
""VolumeID""
|
|
)
|
|
SELECT
|
|
""Block"".""ID"",
|
|
""Block"".""VolumeID""
|
|
FROM ""Block""
|
|
WHERE
|
|
""Block"".""Hash"" = @Hash
|
|
AND ""Block"".""Size"" = @Size
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
blocklist.m_assignBlocksToNewVolume = await connection.CreateCommandAsync(@"
|
|
UPDATE ""Block""
|
|
SET ""VolumeID"" = @TargetVolumeId
|
|
WHERE
|
|
""Hash"" = @Hash
|
|
AND ""Size"" = @Size
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
var m_missingBlocksQuery = $@"
|
|
SELECT
|
|
""{blocklist.m_tablename}"".""Hash"",
|
|
""{blocklist.m_tablename}"".""Size""
|
|
FROM ""{blocklist.m_tablename}""
|
|
WHERE ""{blocklist.m_tablename}"".""Restored"" = @Restored ";
|
|
|
|
blocklist.m_missingBlocksCommand =
|
|
await connection.CreateCommandAsync(m_missingBlocksQuery, token)
|
|
.ConfigureAwait(false);
|
|
|
|
blocklist.m_missingBlocksCountCommand =
|
|
await connection.CreateCommandAsync($@"
|
|
SELECT COUNT(*)
|
|
FROM ({m_missingBlocksQuery})
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
return blocklist;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<bool> SetBlockRestored(string hash, long size, long targetVolumeId, CancellationToken token)
|
|
{
|
|
var restored = await m_insertCommand
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@NewRestoredValue", 1)
|
|
.SetParameterValue("@Hash", hash)
|
|
.SetParameterValue("@Size", size)
|
|
.SetParameterValue("@PreviousRestoredValue", 0)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false) == 1;
|
|
|
|
if (restored)
|
|
{
|
|
await m_copyIntoDuplicatedBlocks.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Hash", hash)
|
|
.SetParameterValue("@Size", size)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
var c = await m_assignBlocksToNewVolume.SetTransaction(m_rtr)
|
|
.SetParameterValue("@TargetVolumeId", targetVolumeId)
|
|
.SetParameterValue("@Hash", hash)
|
|
.SetParameterValue("@Size", size)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (c != 1)
|
|
throw new Exception($"Unexpected number of updated blocks: {c} != 1");
|
|
}
|
|
|
|
return restored;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async IAsyncEnumerable<BlockWithSourceData> GetSourceFilesWithBlocks(long blocksize, [EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
var str_blocksize = Library.Utility.Utility.FormatInvariantValue(blocksize);
|
|
await using var cmd = m_connection.CreateCommand($@"
|
|
SELECT DISTINCT
|
|
""{m_tablename}"".""Hash"",
|
|
""{m_tablename}"".""Size"",
|
|
""File"".""Path"",
|
|
""BlocksetEntry"".""Index"" * {str_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"" * {str_blocksize}
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Restored", 0);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async IAsyncEnumerable<BlockWithMetadataSourceData> GetSourceItemsWithMetadataBlocks([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand($@"
|
|
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""
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Restored", 0);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async IAsyncEnumerable<BlocklistHashesEntry> GetBlocklistHashes(long hashesPerBlock, [EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr);
|
|
|
|
var blocklistTableName = $"BlocklistHashList-{Library.Utility.Utility.GetHexGuid()}";
|
|
|
|
try
|
|
{
|
|
// We need to create a snapshot as we will be updating the m_tablename table during enumeration
|
|
await cmd.SetCommandAndParameters($@"
|
|
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)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
cmd.SetCommandAndParameters($@"
|
|
SELECT
|
|
""BlockHash"",
|
|
""BlocksetId"",
|
|
""BlocksetLength"",
|
|
""BlocklistHashIndex"",
|
|
""BlocklistHashHash"",
|
|
""BlocksetEntryIndex""
|
|
FROM ""{blocklistTableName}""
|
|
ORDER BY
|
|
""BlocksetId"",
|
|
""BlocklistHashIndex"",
|
|
""BlocksetEntryIndex""
|
|
");
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
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
|
|
{
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
DROP TABLE IF EXISTS ""{blocklistTableName}""
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<long> GetMissingBlockCount(CancellationToken token)
|
|
{
|
|
return await m_missingBlocksCountCommand
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Restored", 0)
|
|
.ExecuteScalarInt64Async(0, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the list of missing blocks.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of tuples containing the block hash and size.</returns>
|
|
public async IAsyncEnumerable<(string Hash, long Size)> GetMissingBlocks([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
m_missingBlocksCommand
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Restored", 0);
|
|
|
|
await foreach (var rd in m_missingBlocksCommand.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return (
|
|
rd.ConvertValueToString(0) ?? "",
|
|
rd.ConvertValueToInt64(1)
|
|
);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<long> MoveBlocksToNewVolume(long targetVolumeId, long sourceVolumeId, CancellationToken token)
|
|
{
|
|
if (targetVolumeId <= 0)
|
|
throw new ArgumentOutOfRangeException(nameof(targetVolumeId), "Target volume ID must be greater than 0");
|
|
|
|
// Move the source blocks into the DuplicateBlock table
|
|
await using var cmd = m_connection.CreateCommand($@"
|
|
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 = await cmd.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
// Then update the blocks table to point to the new volume
|
|
var updated = await cmd.SetCommandAndParameters($@"
|
|
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)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (updated != moved)
|
|
throw new Exception($"Unexpected number of updated blocks: {updated} != {moved}");
|
|
|
|
return updated;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async IAsyncEnumerable<IRemoteVolume> GetFilesetsUsingMissingBlocks([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
var blocks = $@"
|
|
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 = $@"
|
|
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 = $@"
|
|
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}
|
|
)
|
|
)
|
|
";
|
|
|
|
await using var cmd = m_connection.CreateCommand(cmdtxt)
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Files.ToString());
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return new RemoteVolume(
|
|
rd.ConvertValueToString(0) ?? "",
|
|
rd.ConvertValueToString(1) ?? "",
|
|
rd.ConvertValueToInt64(2)
|
|
);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async IAsyncEnumerable<IRemoteVolume> GetMissingBlockSources([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand($@"
|
|
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
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Restored", 0)
|
|
.SetParameterValue("@Name", m_volumename);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return new RemoteVolume(
|
|
rd.ConvertValueToString(0) ?? "",
|
|
rd.ConvertValueToString(1) ?? "",
|
|
rd.ConvertValueToInt64(2)
|
|
);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (m_isDisposed)
|
|
return;
|
|
|
|
DisposeAsync().AsTask().Await();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (m_isDisposed)
|
|
return;
|
|
|
|
m_isDisposed = true;
|
|
try
|
|
{
|
|
if (m_tablename != null)
|
|
{
|
|
|
|
await using var cmd = await m_connection.CreateCommandAsync($@"DROP TABLE IF EXISTS ""{m_tablename}""", default)
|
|
.ConfigureAwait(false);
|
|
|
|
await cmd.SetTransaction(m_rtr)
|
|
.ExecuteNonQueryAsync()
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
try { m_insertCommand?.Dispose(); }
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new missing block list for the specified volume.
|
|
/// </summary>
|
|
/// <param name="volumename">The name of the volume with missing blocks.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited returns an instance of <see cref="IMissingBlockList"/>.</returns>
|
|
public async Task<IMissingBlockList> CreateBlockList(string volumename, CancellationToken token)
|
|
{
|
|
return await MissingBlockList.CreateMissingBlockList(volumename, m_connection, m_rtr, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes duplicate metadata hashes in the database.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when completed indicates that the repair has been attempted.</returns>
|
|
public async Task FixDuplicateMetahash(CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
|
|
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 = await cmd.ExecuteScalarInt64Async(sql_count, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (x > 1)
|
|
{
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateMetadataHashes", "Found duplicate metadatahashes, repairing");
|
|
|
|
var tablename = $"TmpFile-{Library.Utility.Utility.GetHexGuid()}";
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
CREATE TEMPORARY TABLE ""{tablename}""
|
|
AS SELECT *
|
|
FROM ""File""
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
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""
|
|
";
|
|
|
|
await using (var c2 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
{
|
|
c2.SetCommandAndParameters($@"
|
|
UPDATE ""{tablename}""
|
|
SET ""MetadataID"" = @MetadataId
|
|
WHERE ""MetadataID"" IN (
|
|
SELECT ""ID""
|
|
FROM ""Metadataset""
|
|
WHERE ""BlocksetID"" = @BlocksetId
|
|
);
|
|
DELETE FROM ""Metadataset""
|
|
WHERE
|
|
""BlocksetID"" = @BlocksetId
|
|
AND ""ID"" != @MetadataId
|
|
");
|
|
|
|
await using var rd = await cmd.ExecuteReaderAsync(sql, token)
|
|
.ConfigureAwait(false);
|
|
|
|
while (await rd.ReadAsync(token).ConfigureAwait(false))
|
|
{
|
|
await c2
|
|
.SetParameterValue("@MetadataId", rd.GetValue(0))
|
|
.SetParameterValue("@BlocksetId", rd.GetValue(1))
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
sql = $@"
|
|
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""
|
|
";
|
|
|
|
await using (var c2 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
{
|
|
c2.SetCommandAndParameters($@"
|
|
UPDATE ""FilesetEntry""
|
|
SET ""FileID"" = @FileId
|
|
WHERE ""FileID"" IN (
|
|
SELECT ""ID""
|
|
FROM ""{tablename}""
|
|
WHERE
|
|
""Path"" = @Path
|
|
AND ""BlocksetID"" = @BlocksetId
|
|
AND ""MetadataID"" = @MetadataId
|
|
);
|
|
DELETE FROM ""{tablename}""
|
|
WHERE
|
|
""Path"" = @Path
|
|
AND ""BlocksetID"" = @BlocksetId
|
|
AND ""MetadataID"" = @MetadataId
|
|
AND ""ID"" != @FileId
|
|
");
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(sql, token).ConfigureAwait(false))
|
|
{
|
|
await c2
|
|
.SetParameterValue("@FileId", rd.GetValue(0))
|
|
.SetParameterValue("@Path", rd.GetValue(1))
|
|
.SetParameterValue("@BlocksetId", rd.GetValue(2))
|
|
.SetParameterValue("@MetadataId", rd.GetValue(3))
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
DELETE FROM ""FileLookup""
|
|
WHERE ""ID"" NOT IN (
|
|
SELECT ""ID""
|
|
FROM ""{tablename}""
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
CREATE INDEX ""{tablename}-Ix""
|
|
ON ""{tablename}"" (
|
|
""ID"",
|
|
""MetadataID""
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
UPDATE ""FileLookup""
|
|
SET ""MetadataID"" = (
|
|
SELECT ""MetadataID""
|
|
FROM ""{tablename}"" ""A""
|
|
WHERE ""A"".""ID"" = ""FileLookup"".""ID""
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
await cmd.ExecuteNonQueryAsync($@"DROP TABLE ""{tablename}"" ", token)
|
|
.ConfigureAwait(false);
|
|
|
|
x = await cmd.ExecuteScalarInt64Async(sql_count, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (x > 1)
|
|
throw new Interface.UserInformationException("Repair failed, there are still duplicate metadatahashes!", "DuplicateHashesRepairFailed");
|
|
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateMetadataHashesFixed", "Duplicate metadatahashes repaired succesfully");
|
|
|
|
await m_rtr.CommitAsync(token: token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes duplicate file entries in the database.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when completed indicates that the repair has been attempted.</returns>
|
|
public async Task FixDuplicateFileentries(CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
|
|
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 = await cmd.ExecuteScalarInt64Async(sql_count, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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""
|
|
";
|
|
|
|
await using (var c2 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
{
|
|
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
|
|
");
|
|
|
|
cmd.SetCommandAndParameters(sql);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
await 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))
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
x = await cmd.ExecuteScalarInt64Async(sql_count, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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");
|
|
|
|
await m_rtr.CommitAsync(token: token).ConfigureAwait(false);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes duplicate paths in filesets by removing duplicate entries,
|
|
/// keeping the entry with the highest FileID (most recent).
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when completed indicates that the repair has been attempted.</returns>
|
|
public async Task FixDuplicatePathsInFilesets(CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
|
|
// Count duplicates across all filesets
|
|
var countSql = @"
|
|
SELECT COUNT(*) FROM (
|
|
SELECT
|
|
fe.""FilesetID"",
|
|
f.""Path"",
|
|
COUNT(*) as cnt
|
|
FROM ""FilesetEntry"" fe
|
|
JOIN ""File"" f ON fe.""FileID"" = f.""ID""
|
|
GROUP BY fe.""FilesetID"", f.""Path""
|
|
HAVING cnt > 1
|
|
)
|
|
";
|
|
|
|
var duplicateCount = await cmd.ExecuteScalarInt64Async(countSql, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (duplicateCount > 0)
|
|
{
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "DuplicatePathsInFilesets",
|
|
"Found {0} duplicate paths across filesets, repairing", duplicateCount);
|
|
|
|
// Delete duplicates, keeping the one with highest FileID in each fileset
|
|
// Note: FilesetEntry doesn't have an ID column, so we use the composite key
|
|
var deleteSql = @"
|
|
DELETE FROM ""FilesetEntry""
|
|
WHERE (""FilesetID"", ""FileID"") IN (
|
|
SELECT ""FilesetID"", ""FileID"" FROM (
|
|
SELECT
|
|
fe.""FilesetID"",
|
|
fe.""FileID"",
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY fe.""FilesetID"", f.""Path""
|
|
ORDER BY fe.""FileID"" DESC
|
|
) as rn
|
|
FROM ""FilesetEntry"" fe
|
|
JOIN ""File"" f ON fe.""FileID"" = f.""ID""
|
|
) WHERE rn > 1
|
|
)
|
|
";
|
|
|
|
var deletedCount = await cmd.ExecuteNonQueryAsync(deleteSql, token)
|
|
.ConfigureAwait(false);
|
|
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "DuplicatePathsInFilesetsFixed",
|
|
"Removed {0} duplicate path entries from filesets", deletedCount);
|
|
|
|
await m_rtr.CommitAsync(token: token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes missing blocklist hashes in the database.
|
|
/// </summary>
|
|
/// <param name="blockhashalgorithm">The hash algorithm used for the blocklist hashes.</param>
|
|
/// <param name="blocksize">The size of each block in bytes.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when completed indicates that the repair has been attempted.</returns>
|
|
public async Task FixMissingBlocklistHashes(string blockhashalgorithm, long blocksize, CancellationToken token)
|
|
{
|
|
var blocklistbuffer = new byte[blocksize];
|
|
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
using var blockhasher = HashFactory.CreateHasher(blockhashalgorithm);
|
|
|
|
var hashsize = blockhasher.HashSize / 8;
|
|
var blocksize_per_hashsize = Library.Utility.Utility.FormatInvariantValue(blocksize / hashsize);
|
|
|
|
var sql = $@"
|
|
SELECT *
|
|
FROM (
|
|
SELECT
|
|
""N"".""BlocksetID"",
|
|
((""N"".""BlockCount"" + {blocksize_per_hashsize} - 1) / {blocksize_per_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 = await cmd
|
|
.ExecuteScalarInt64Async(countsql, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (itemswithnoblocklisthash != 0)
|
|
{
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "MissingBlocklistHashes", "Found {0} missing blocklisthash entries, repairing", itemswithnoblocklisthash);
|
|
await using (var c2 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
await using (var c3 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
await using (var c4 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
await using (var c5 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
await using (var c6 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
{
|
|
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;
|
|
");
|
|
|
|
await foreach (var e in cmd.ExecuteReaderEnumerableAsync(sql, token).ConfigureAwait(false))
|
|
{
|
|
var blocksetid = e.ConvertValueToInt64(0);
|
|
var ix = 0L;
|
|
int blocklistoffset = 0;
|
|
|
|
await c2.SetCommandAndParameters(@"
|
|
DELETE FROM ""BlocklistHash""
|
|
WHERE ""BlocksetID"" = @BlocksetId
|
|
")
|
|
.SetParameterValue("@BlocksetId", blocksetid)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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);
|
|
|
|
await foreach (var h in c2.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
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 = await c4
|
|
.SetParameterValue("@Hash", blkey)
|
|
.SetParameterValue("@Size", blocklistoffset)
|
|
.ExecuteScalarInt64Async(-1, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existingBlockId <= 0)
|
|
{
|
|
var deletedBlockId = await c5
|
|
.SetParameterValue("@Hash", blkey)
|
|
.SetParameterValue("@Size", blocklistoffset)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString())
|
|
.SetParameterValue("@State1", RemoteVolumeState.Uploaded.ToString())
|
|
.SetParameterValue("@State2", RemoteVolumeState.Verified.ToString())
|
|
.ExecuteScalarInt64Async(-1, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (deletedBlockId <= 0)
|
|
throw new Exception($"Missing block for blocklisthash: {blkey}");
|
|
else
|
|
{
|
|
var rc = await c6
|
|
.SetParameterValue("@DeletedBlockId", deletedBlockId)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (rc != 2)
|
|
throw new Exception($"Unexpected update count: {rc}");
|
|
}
|
|
}
|
|
|
|
// Add to table
|
|
await c3.SetParameterValue("@BlocksetId", blocksetid)
|
|
.SetParameterValue("@Index", ix)
|
|
.SetParameterValue("@Hash", blkey)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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 = await c4
|
|
.SetParameterValue("@Hash", blkeyfinal)
|
|
.SetParameterValue("@Size", blocklistoffset)
|
|
.ExecuteScalarInt64Async(-1, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existingBlockId <= 0)
|
|
{
|
|
var deletedBlockId = await c5
|
|
.SetParameterValue("@Hash", blkeyfinal)
|
|
.SetParameterValue("@Size", blocklistoffset)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString())
|
|
.SetParameterValue("@State1", RemoteVolumeState.Uploaded.ToString())
|
|
.SetParameterValue("@State2", RemoteVolumeState.Verified.ToString())
|
|
.ExecuteScalarInt64Async(-1, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (deletedBlockId == 0)
|
|
throw new Exception($"Missing block for blocklisthash: {blkeyfinal}");
|
|
else
|
|
{
|
|
var rc = await c6
|
|
.SetParameterValue("@DeletedBlockId", deletedBlockId)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
if (rc != 2)
|
|
throw new Exception($"Unexpected update count: {rc}");
|
|
}
|
|
}
|
|
|
|
// Add to table
|
|
await c3
|
|
.SetParameterValue("@BlocksetId", blocksetid)
|
|
.SetParameterValue("@Index", ix)
|
|
.SetParameterValue("@Hash", blkeyfinal)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
itemswithnoblocklisthash = await cmd
|
|
.ExecuteScalarInt64Async(countsql, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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");
|
|
|
|
await m_rtr.CommitAsync(token: token).ConfigureAwait(false);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes duplicate blocklist hashes in the database.
|
|
/// </summary>
|
|
/// <param name="blocksize">The size of each block in bytes.</param>
|
|
/// <param name="hashsize">The size of each hash in bytes.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when completed indicates that the repair has been attempted.</returns>
|
|
public async Task FixDuplicateBlocklistHashes(long blocksize, long hashsize, CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
|
|
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 = await cmd.ExecuteScalarInt64Async(sql_count, 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (x > 0)
|
|
{
|
|
Logging.Log.WriteInformationMessage(LOGTAG, "DuplicateBlocklistHashes", "Found duplicate blocklisthash entries, repairing");
|
|
|
|
var unique_count = await cmd.ExecuteScalarInt64Async(@"
|
|
SELECT COUNT(*)
|
|
FROM (
|
|
SELECT DISTINCT
|
|
""BlocksetID"",
|
|
""Index""
|
|
FROM ""BlocklistHash""
|
|
)
|
|
", 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
await using (var c2 = m_connection.CreateCommand(m_rtr.Transaction))
|
|
{
|
|
c2.SetCommandAndParameters(@"
|
|
DELETE FROM ""BlocklistHash""
|
|
WHERE rowid IN (
|
|
SELECT rowid
|
|
FROM ""BlocklistHash""
|
|
WHERE
|
|
""BlocksetID"" = @BlocksetId
|
|
AND ""Index"" = @Index
|
|
LIMIT @Limit
|
|
)
|
|
");
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(dup_sql, token).ConfigureAwait(false))
|
|
{
|
|
var expected = rd.GetInt32(2) - 1;
|
|
var actual = await c2
|
|
.SetParameterValue("@BlocksetId", rd.GetValue(0))
|
|
.SetParameterValue("@Index", rd.GetValue(1))
|
|
.SetParameterValue("@Limit", expected)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (actual != expected)
|
|
throw new Exception($"Unexpected number of results after fix, got: {actual}, expected: {expected}");
|
|
}
|
|
}
|
|
|
|
x = await cmd.ExecuteScalarInt64Async(sql_count, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (x > 1)
|
|
throw new Exception("Repair failed, there are still duplicate file entries!");
|
|
|
|
var real_count = await cmd.ExecuteScalarInt64Async(@"
|
|
SELECT Count(*)
|
|
FROM ""BlocklistHash""
|
|
", 0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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
|
|
{
|
|
await VerifyConsistency(blocksize, hashsize, true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
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");
|
|
|
|
await m_rtr.CommitAsync(token: token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if all blocks in the specified volume are present in the database.
|
|
/// </summary>
|
|
/// <param name="filename">The name of the volume to check.</param>
|
|
/// <param name="blocks">A collection of blocks to check, represented as key-value pairs where the key is the block hash and the value is the block size.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <exception cref="Exception">Thrown if not all blocks are found in the specified volume.</exception>
|
|
/// <returns>A task that when awaited indicates the completion of the check.</returns>
|
|
public async Task CheckAllBlocksAreInVolume(string filename, IEnumerable<KeyValuePair<string, long>> blocks, CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
var tablename = $"ProbeBlocks-{Library.Utility.Utility.GetHexGuid()}";
|
|
|
|
try
|
|
{
|
|
await cmd.ExecuteNonQueryAsync($@"
|
|
CREATE TEMPORARY TABLE ""{tablename}"" (
|
|
""Hash"" TEXT NOT NULL,
|
|
""Size"" INTEGER NOT NULL
|
|
)
|
|
", token)
|
|
.ConfigureAwait(false);
|
|
|
|
cmd.SetCommandAndParameters($@"
|
|
INSERT INTO ""{tablename}"" (
|
|
""Hash"",
|
|
""Size""
|
|
)
|
|
VALUES (
|
|
@Hash,
|
|
@Size
|
|
)
|
|
");
|
|
|
|
foreach (var kp in blocks)
|
|
{
|
|
await cmd
|
|
.SetParameterValue("@Hash", kp.Key)
|
|
.SetParameterValue("@Size", kp.Value)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
var id = await cmd.SetCommandAndParameters(@"
|
|
SELECT ""ID""
|
|
FROM ""RemoteVolume""
|
|
WHERE ""Name"" = @Name
|
|
")
|
|
.SetParameterValue("@Name", filename)
|
|
.ExecuteScalarInt64Async(-1, token)
|
|
.ConfigureAwait(false);
|
|
|
|
var aliens = await cmd.SetCommandAndParameters($@"
|
|
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)
|
|
.ExecuteScalarInt64Async(0, token)
|
|
.ConfigureAwait(false);
|
|
|
|
if (aliens != 0)
|
|
throw new Exception($"Not all blocks were found in {filename}");
|
|
}
|
|
finally
|
|
{
|
|
await cmd.ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{tablename}"" ", token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the provided blocklist matches the expected entries in the database for a given block hash and size.
|
|
/// </summary>
|
|
/// <param name="hash">The hash of the blocklist to check.</param>
|
|
/// <param name="length">The size of the blocklist in bytes.</param>
|
|
/// <param name="blocklist">The expected blocklist entries to compare against.</param>
|
|
/// <param name="blocksize">The size of each block in bytes.</param>
|
|
/// <param name="blockhashlength">The length of each block hash in bytes.</param>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>A task that when awaited indicates the completion of the check.</returns>
|
|
/// <exception cref="Exception">Thrown if the blocklist does not match the expected entries.</exception>
|
|
public async Task CheckBlocklistCorrect(string hash, long length, IEnumerable<string> blocklist, long blocksize, long blockhashlength, CancellationToken token)
|
|
{
|
|
await using var cmd = m_connection.CreateCommand(m_rtr.Transaction);
|
|
|
|
var str_blocksize = Library.Utility.Utility.FormatInvariantValue(blocksize);
|
|
var str_blockhashlength = Library.Utility.Utility.FormatInvariantValue(blockhashlength);
|
|
|
|
var query = $@"
|
|
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"" * ({str_blocksize} / {str_blockhashlength})
|
|
AND ""A"".""Index"" < (""B"".""BlocklistHashIndex"" + 1) * ({str_blocksize} / {str_blockhashlength})
|
|
ORDER BY ""A"".""Index""
|
|
";
|
|
|
|
using var en = blocklist.GetEnumerator();
|
|
|
|
cmd.SetCommandAndParameters(query)
|
|
.SetParameterValue("@Hash", hash)
|
|
.SetParameterValue("@Size", length);
|
|
|
|
await foreach (var r in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
{
|
|
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}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if there are any missing local filesets in the database.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of missing local fileset names.</returns>
|
|
public async IAsyncEnumerable<string> MissingLocalFilesets([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await 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""
|
|
)
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Files.ToString())
|
|
.ExpandInClauseParameterMssqlite("@States", [
|
|
RemoteVolumeState.Deleting.ToString(),
|
|
RemoteVolumeState.Deleted.ToString()
|
|
]);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return rd.ConvertValueToString(0) ?? "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if there are any missing remote filesets in the database.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of tuples containing the fileset ID, timestamp, and whether it is a full backup.</returns>
|
|
public async IAsyncEnumerable<(long FilesetID, DateTime Timestamp, bool IsFull)> MissingRemoteFilesets([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await 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)
|
|
)
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Files.ToString())
|
|
.ExpandInClauseParameterMssqlite("@States", [
|
|
RemoteVolumeState.Deleting.ToString(),
|
|
RemoteVolumeState.Deleted.ToString()
|
|
]);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return (
|
|
rd.ConvertValueToInt64(0),
|
|
ParseFromEpochSeconds(rd.ConvertValueToInt64(1)),
|
|
rd.ConvertValueToInt64(2) == BackupType.FULL_BACKUP
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if there are any empty index files in the database.
|
|
/// </summary>
|
|
/// <param name="token">A cancellation token to cancel the operation.</param>
|
|
/// <returns>An asynchronous enumerable of remote volumes that are empty index files.</returns>
|
|
public async IAsyncEnumerable<IRemoteVolume> EmptyIndexFiles([EnumeratorCancellation] CancellationToken token)
|
|
{
|
|
await 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""
|
|
)
|
|
")
|
|
.SetTransaction(m_rtr)
|
|
.SetParameterValue("@Type", RemoteVolumeType.Index.ToString())
|
|
.ExpandInClauseParameterMssqlite("@States", [
|
|
RemoteVolumeState.Uploaded.ToString(),
|
|
RemoteVolumeState.Verified.ToString()
|
|
]);
|
|
|
|
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(token).ConfigureAwait(false))
|
|
yield return new RemoteVolume(
|
|
rd.ConvertValueToString(0) ?? "",
|
|
rd.ConvertValueToString(1) ?? "",
|
|
rd.ConvertValueToInt64(2)
|
|
);
|
|
}
|
|
|
|
public async Task FixEmptyMetadatasets(Options options, CancellationToken token)
|
|
{
|
|
using var cmd = m_connection.CreateCommand(@"
|
|
SELECT COUNT(*)
|
|
FROM ""Metadataset""
|
|
JOIN ""Blockset""
|
|
ON ""Metadataset"".""BlocksetID"" = ""Blockset"".""ID""
|
|
WHERE ""Blockset"".""Length"" = 0
|
|
")
|
|
.SetTransaction(m_rtr);
|
|
|
|
var emptyMetaCount = await cmd.ExecuteScalarInt64Async(0, token)
|
|
.ConfigureAwait(false);
|
|
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<string, string>(), options);
|
|
var emptyBlocksetId = await GetEmptyMetadataBlocksetId(Array.Empty<long>(), emptyMeta.FileHash, emptyMeta.Blob.Length, token)
|
|
.ConfigureAwait(false);
|
|
|
|
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.GetHexGuid()}";
|
|
|
|
try
|
|
{
|
|
await 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)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
// Step 2: Update FileLookup to use a valid metadata ID
|
|
await 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)
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
// Step 3: Delete obsolete Metadataset entries
|
|
await cmd.SetCommandAndParameters(@$"
|
|
DELETE FROM ""Metadataset""
|
|
WHERE ""ID"" IN (
|
|
SELECT ""MetadataID""
|
|
FROM ""{tablename}""
|
|
)
|
|
")
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
// Step 4: Delete orphaned blocksets (affected only)
|
|
await 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""
|
|
)
|
|
")
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
|
|
// 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 = await cmd.ExecuteScalarInt64Async(0, token);
|
|
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");
|
|
|
|
await m_rtr.CommitAsync(token: token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
await cmd.SetCommandAndParameters($@"DROP TABLE IF EXISTS ""{tablename}"" ")
|
|
.ExecuteNonQueryAsync(true, token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.Log.WriteVerboseMessage(LOGTAG, "ErrorDroppingTempTable", ex, "Failed to drop temporary table {0}: {1}", tablename, ex.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|