Removed superfluous namespaces. Using `var` most places. Using extension methods for common setup of commands.
720 lines
39 KiB
C#
720 lines
39 KiB
C#
// 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.
|
|
using System;
|
|
using System.Data;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Duplicati.Library.Main.Database
|
|
{
|
|
internal class LocalRecreateDatabase : LocalRestoreDatabase
|
|
{
|
|
/// <summary>
|
|
/// The tag used for logging
|
|
/// </summary>
|
|
private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalRecreateDatabase));
|
|
|
|
private class PathEntryKeeper
|
|
{
|
|
private struct KeyValueComparer : IComparer<KeyValuePair<long, long>>
|
|
{
|
|
public int Compare(KeyValuePair<long, long> x, KeyValuePair<long, long> y)
|
|
{
|
|
return x.Key == y.Key ?
|
|
(x.Value == y.Value ?
|
|
0
|
|
: (x.Value < y.Value ? -1 : 1))
|
|
: (x.Key < y.Key ? -1 : 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly IDbCommand m_insertFileCommand;
|
|
private readonly IDbCommand m_insertFilesetEntryCommand;
|
|
private readonly IDbCommand m_insertMetadatasetCommand;
|
|
private readonly IDbCommand m_insertBlocksetCommand;
|
|
private readonly IDbCommand m_insertBlocklistHashCommand;
|
|
private readonly IDbCommand m_updateBlockVolumeCommand;
|
|
private readonly IDbCommand m_insertTempBlockListHash;
|
|
private readonly IDbCommand m_insertSmallBlockset;
|
|
private readonly IDbCommand m_findBlocksetCommand;
|
|
private readonly IDbCommand m_findMetadatasetCommand;
|
|
private readonly IDbCommand m_findFilesetCommand;
|
|
private readonly IDbCommand m_findTempBlockListHashCommand;
|
|
private readonly IDbCommand m_findHashBlockCommand;
|
|
private readonly IDbCommand m_insertBlockCommand;
|
|
private readonly IDbCommand m_insertDuplicateBlockCommand;
|
|
|
|
private string m_tempblocklist;
|
|
private string m_tempsmalllist;
|
|
|
|
/// <summary>
|
|
/// A lookup table that prevents multiple downloads of the same volume
|
|
/// </summary>
|
|
private Dictionary<long, long> m_proccessedVolumes;
|
|
|
|
// SQL that finds index and block size for all blocklist hashes, based on the temporary hash list
|
|
// with vars Used:
|
|
// {0} --> Blocksize
|
|
// {1} --> BlockHash-Size
|
|
// {2} --> Temp-Table
|
|
// {3} --> FullBlocklist-BlockCount [equals ({0} / {1}), if SQLite pays respect to ints]
|
|
private static string SELECT_BLOCKLIST_ENTRIES(long blocksize, long blockhashsize, string temptable, long fullBlockListBlockCount) =>
|
|
FormatInvariant($@"
|
|
SELECT
|
|
""E"".""BlocksetID"",
|
|
""F"".""Index"" + (""E"".""BlocklistIndex"" * {fullBlockListBlockCount}) AS ""FullIndex"",
|
|
""F"".""BlockHash"",
|
|
MIN({blocksize}, ""E"".""Length"" - ((""F"".""Index"" + (""E"".""BlocklistIndex"" * {fullBlockListBlockCount})) * {blocksize})) AS ""BlockSize"",
|
|
""E"".""Hash"",
|
|
""E"".""BlocklistSize"",
|
|
""E"".""BlocklistHash""
|
|
FROM
|
|
(
|
|
SELECT * FROM
|
|
(
|
|
SELECT
|
|
""A"".""BlocksetID"",
|
|
""A"".""Index"" AS ""BlocklistIndex"",
|
|
MIN({fullBlockListBlockCount} * {blockhashsize}, (((""B"".""Length"" + {blocksize} - 1) / {blocksize}) - (""A"".""Index"" * ({fullBlockListBlockCount}))) * {blockhashsize}) AS ""BlocklistSize"",
|
|
""A"".""Hash"" AS ""BlocklistHash"",
|
|
""B"".""Length""
|
|
FROM
|
|
""BlocklistHash"" A,
|
|
""Blockset"" B
|
|
WHERE
|
|
""B"".""ID"" = ""A"".""BlocksetID""
|
|
) C,
|
|
""Block"" D
|
|
WHERE
|
|
""C"".""BlocklistHash"" = ""D"".""Hash""
|
|
AND
|
|
""C"".""BlocklistSize"" = ""D"".""Size""
|
|
) E,
|
|
""{temptable}"" F
|
|
WHERE
|
|
""F"".""BlocklistHash"" = ""E"".""Hash""
|
|
ORDER BY
|
|
""E"".""BlocksetID"",
|
|
""FullIndex""
|
|
");
|
|
|
|
public LocalRecreateDatabase(LocalDatabase parentdb, Options options)
|
|
: base(parentdb)
|
|
{
|
|
m_tempblocklist = "TempBlocklist_" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
|
|
m_tempsmalllist = "TempSmalllist_" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
|
|
|
|
using (var cmd = m_connection.CreateCommand())
|
|
{
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{m_tempblocklist}"" (""BlockListHash"" TEXT NOT NULL, ""BlockHash"" TEXT NOT NULL, ""Index"" INTEGER NOT NULL)"));
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE INDEX ""Index_{m_tempblocklist}"" ON ""{m_tempblocklist}"" (""BlockListHash"");"));
|
|
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{m_tempsmalllist}"" (""FileHash"" TEXT NOT NULL, ""BlockHash"" TEXT NOT NULL, ""BlockSize"" INTEGER NOT NULL)"));
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE UNIQUE INDEX ""Index_File_{m_tempsmalllist}"" ON ""{m_tempsmalllist}"" (""FileHash"", ""BlockSize"");"));
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE UNIQUE INDEX ""Index_Block_{m_tempsmalllist}"" ON ""{m_tempsmalllist}"" (""BlockHash"", ""BlockSize"");"));
|
|
}
|
|
|
|
m_insertFileCommand = m_connection.CreateCommand(@"INSERT INTO ""FileLookup"" (""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"") VALUES (?,?,?,?); SELECT last_insert_rowid();");
|
|
m_insertFilesetEntryCommand = m_connection.CreateCommand(@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") VALUES (?,?,?)");
|
|
m_insertMetadatasetCommand = m_connection.CreateCommand(@"INSERT INTO ""Metadataset"" (""BlocksetID"") VALUES (?); SELECT last_insert_rowid();");
|
|
m_insertBlocksetCommand = m_connection.CreateCommand(@"INSERT INTO ""Blockset"" (""Length"", ""FullHash"") VALUES (?,?); SELECT last_insert_rowid();");
|
|
m_insertBlocklistHashCommand = m_connection.CreateCommand(@"INSERT INTO ""BlocklistHash"" (""BlocksetID"", ""Index"", ""Hash"") VALUES (?,?,?)");
|
|
m_updateBlockVolumeCommand = m_connection.CreateCommand(@"UPDATE ""Block"" SET ""VolumeID"" = ? WHERE ""Hash"" = ? AND ""Size"" = ?");
|
|
m_insertTempBlockListHash = m_connection.CreateCommand(FormatInvariant($@"INSERT INTO ""{m_tempblocklist}"" (""BlocklistHash"", ""BlockHash"", ""Index"") VALUES (?,?,?) "));
|
|
m_insertSmallBlockset = m_connection.CreateCommand(FormatInvariant($@"INSERT OR IGNORE INTO ""{m_tempsmalllist}"" (""FileHash"", ""BlockHash"", ""BlockSize"") VALUES (?,?,?) "));
|
|
m_findBlocksetCommand = m_connection.CreateCommand(@"SELECT ""ID"" FROM ""Blockset"" WHERE ""Length"" = ? AND ""FullHash"" = ? ");
|
|
m_findMetadatasetCommand = m_connection.CreateCommand(@"SELECT ""Metadataset"".""ID"" FROM ""Metadataset"",""Blockset"" WHERE ""Metadataset"".""BlocksetID"" = ""Blockset"".""ID"" AND ""Blockset"".""FullHash"" = ? AND ""Blockset"".""Length"" = ? ");
|
|
m_findFilesetCommand = m_connection.CreateCommand(@"SELECT ""ID"" FROM ""FileLookup"" WHERE ""PrefixID"" = ? AND ""Path"" = ? AND ""BlocksetID"" = ? AND ""MetadataID"" = ? ");
|
|
m_findTempBlockListHashCommand = m_connection.CreateCommand(FormatInvariant($@"SELECT DISTINCT ""BlockListHash"" FROM ""{m_tempblocklist}"" WHERE ""BlockListHash"" = ? "));
|
|
m_findHashBlockCommand = m_connection.CreateCommand(@"SELECT ""VolumeID"" FROM ""Block"" WHERE ""Hash"" = ? AND ""Size"" = ? ");
|
|
m_insertBlockCommand = m_connection.CreateCommand(@"INSERT INTO ""Block"" (""Hash"", ""Size"", ""VolumeID"") VALUES (?,?,?)");
|
|
m_insertDuplicateBlockCommand = m_connection.CreateCommand(@"INSERT OR IGNORE INTO ""DuplicateBlock"" (""BlockID"", ""VolumeID"") VALUES ((SELECT ""ID"" FROM ""Block"" WHERE ""Hash"" = ? AND ""Size"" = ?), ?)");
|
|
}
|
|
|
|
public void FindMissingBlocklistHashes(long hashsize, long blocksize, IDbTransaction transaction)
|
|
{
|
|
using (var cmd = m_connection.CreateCommand(transaction))
|
|
{
|
|
//Update all small blocklists and matching blocks
|
|
|
|
var selectSmallBlocks = FormatInvariant($@"SELECT ""BlockHash"", ""BlockSize"" FROM ""{m_tempsmalllist}""");
|
|
|
|
var selectBlockHashes = FormatInvariant($@"SELECT ""BlockHash"" AS ""FullHash"", ""BlockSize"" AS ""Length"" FROM ( {SELECT_BLOCKLIST_ENTRIES(blocksize, hashsize, m_tempblocklist, blocksize / hashsize)} )");
|
|
|
|
var selectAllBlocks = @"SELECT DISTINCT ""FullHash"", ""Length"" FROM (" + selectBlockHashes + " UNION " + selectSmallBlocks + " )";
|
|
|
|
var selectNewBlocks = FormatInvariant($@"SELECT ""FullHash"" AS ""Hash"", ""Length"" AS ""Size"", -1 AS ""VolumeID""
|
|
FROM (SELECT ""A"".""FullHash"", ""A"".""Length"", CASE WHEN ""B"".""Hash"" IS NULL THEN '' ELSE ""B"".""Hash"" END AS ""Hash"", CASE WHEN ""B"".""Size"" is NULL THEN -1 ELSE ""B"".""Size"" END AS ""Size"" FROM ({selectAllBlocks}) A
|
|
LEFT OUTER JOIN ""Block"" B ON ""B"".""Hash"" = ""A"".""FullHash"" AND ""B"".""Size"" = ""A"".""Length"" )
|
|
WHERE ""FullHash"" != ""Hash"" AND ""Length"" != ""Size"" ");
|
|
|
|
var insertBlocksCommand =
|
|
@"INSERT INTO ""Block"" (""Hash"", ""Size"", ""VolumeID"") " +
|
|
selectNewBlocks;
|
|
|
|
// Insert all known blocks into block table with volumeid = -1
|
|
cmd.ExecuteNonQuery(insertBlocksCommand);
|
|
|
|
var selectBlocklistBlocksetEntries = FormatInvariant(
|
|
// TODO: The BlocklistHash join seems to be unnecessary, but the join might be required to work around a from really old versions of Duplicati
|
|
// this could be used instead
|
|
//@") D, ""Block"" WHERE ""BlockQuery"".""BlockHash"" = ""Block"".""Hash"" AND ""BlockQuery"".""BlockSize"" = ""Block"".""Size"" ";
|
|
$@"SELECT ""E"".""BlocksetID"" AS ""BlocksetID"", ""D"".""FullIndex"" AS ""Index"", ""F"".""ID"" AS ""BlockID"" FROM ( {SELECT_BLOCKLIST_ENTRIES(blocksize, hashsize, m_tempblocklist, blocksize / hashsize)}) D, ""BlocklistHash"" E, ""Block"" F, ""Block"" G WHERE ""D"".""BlocksetID"" = ""E"".""BlocksetID"" AND ""D"".""BlocklistHash"" = ""E"".""Hash"" AND ""D"".""BlocklistSize"" = ""G"".""Size"" AND ""D"".""BlocklistHash"" = ""G"".""Hash"" AND ""D"".""Blockhash"" = ""F"".""Hash"" AND ""D"".""BlockSize"" = ""F"".""Size"" "
|
|
);
|
|
|
|
var selectBlocksetEntries = FormatInvariant($@"SELECT ""Blockset"".""ID"" AS ""BlocksetID"", 0 AS ""Index"", ""Block"".""ID"" AS ""BlockID"" FROM ""Blockset"", ""Block"", ""{m_tempsmalllist}"" S WHERE ""Blockset"".""Fullhash"" = ""S"".""FileHash"" AND ""S"".""BlockHash"" = ""Block"".""Hash"" AND ""S"".""BlockSize"" = ""Block"".""Size"" AND ""Blockset"".""Length"" = ""S"".""BlockSize"" AND ""Blockset"".""Length"" <= {blocksize} ");
|
|
|
|
var selectAllBlocksetEntries =
|
|
selectBlocklistBlocksetEntries +
|
|
@" UNION " +
|
|
selectBlocksetEntries;
|
|
|
|
var selectFiltered =
|
|
@"SELECT DISTINCT ""H"".""BlocksetID"", ""H"".""Index"", ""H"".""BlockID"" FROM (" +
|
|
selectAllBlocksetEntries +
|
|
@") H WHERE (""H"".""BlocksetID"" || ':' || ""H"".""Index"") NOT IN (SELECT (""ExistingBlocksetEntries"".""BlocksetID"" || ':' || ""ExistingBlocksetEntries"".""Index"") FROM ""BlocksetEntry"" ""ExistingBlocksetEntries"" )";
|
|
|
|
var insertBlocksetEntriesCommand =
|
|
@"INSERT INTO ""BlocksetEntry"" (""BlocksetID"", ""Index"", ""BlockID"") " + selectFiltered;
|
|
|
|
try
|
|
{
|
|
cmd.ExecuteNonQuery(insertBlocksetEntriesCommand);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.Log.WriteErrorMessage(LOGTAG, "BlocksetInsertFailed", ex, "Blockset insert failed, committing temporary tables for trace purposes");
|
|
|
|
using (var fixcmd = m_connection.CreateCommand())
|
|
{
|
|
fixcmd.ExecuteNonQuery(FormatInvariant($@"CREATE TABLE ""{m_tempblocklist}-Failure"" AS SELECT * FROM ""{m_tempblocklist}"" "));
|
|
fixcmd.ExecuteNonQuery(FormatInvariant($@"CREATE TABLE ""{m_tempsmalllist}-Failure"" AS SELECT * FROM ""{m_tempsmalllist}"" "));
|
|
}
|
|
|
|
throw new Exception("The recreate failed, please create a bug-report from this database and send it to the developers for further analysis");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// From the temporary tables 1) insert new blocks into Block (VolumeID to be set at a later stage)
|
|
/// and 2) add missing BlocksetEntry lines
|
|
///
|
|
/// hashsize and blocksize: global database parameters
|
|
/// hashOnly: do not take in account small blocks - these have been added at the
|
|
/// end of the index handling and are not changed in the dblock handling so we can ignore them
|
|
/// </summary>
|
|
/// Notes:
|
|
///
|
|
/// temp block list structure: blocklist hash, block hash, index relative to the
|
|
/// beginning of the blocklist hash (NOT the file)
|
|
///
|
|
/// temp small list structure: filehash, blockhash, blocksize: as the small files are defined
|
|
/// by the fact that they are contained in a single block, blockhash is the same as the filehash,
|
|
/// and blocksize can vary from 0 to the configured block size for the backup
|
|
public void AddBlockAndBlockSetEntryFromTemp(long hashsize, long blocksize, IDbTransaction transaction, bool hashOnly = false)
|
|
{
|
|
|
|
using (var cmd = m_connection.CreateCommand(transaction))
|
|
{
|
|
var insertBlocksCommand = FormatInvariant($@"INSERT INTO BLOCK (Hash, Size, VolumeID)
|
|
SELECT DISTINCT BlockHash AS Hash, BlockSize AS Size, -1 AS VolumeID FROM
|
|
(
|
|
SELECT NB.BlockHash,
|
|
MIN({blocksize}, BS.Length - ((NB.""Index"" + (BH.""Index"" * {blocksize / hashsize})) * {blocksize})) AS BlockSize
|
|
FROM (
|
|
SELECT TBL.BlockListHash, TBL.BlockHash, TBL.""Index"" FROM {m_tempblocklist} TBL
|
|
LEFT OUTER JOIN Block B ON (B.Hash = TBL.BlockHash)
|
|
WHERE B.Hash IS NULL
|
|
) NB
|
|
JOIN BlocklistHash BH ON (BH.Hash = NB.BlocklistHash)
|
|
JOIN Blockset BS ON (BS.ID = BH.Blocksetid) ");
|
|
if (!hashOnly)
|
|
{
|
|
insertBlocksCommand += FormatInvariant($@" UNION
|
|
SELECT TS.BlockHash, TS.BlockSize FROM
|
|
{m_tempsmalllist} TS
|
|
WHERE NOT EXISTS (SELECT ""X"" FROM Block AS B WHERE
|
|
B.Hash = TS.BlockHash AND
|
|
B.Size = TS.BlockSize)
|
|
)");
|
|
}
|
|
|
|
var insertBlocksetEntriesCommand = FormatInvariant($@"INSERT INTO BlocksetEntry (BlocksetID, ""Index"", BlockID)
|
|
SELECT DISTINCT BH.blocksetid, (BH.""Index"" * {blocksize / hashsize})+TBL.""Index"" as FullIndex, BK.ID AS BlockID
|
|
FROM {m_tempblocklist} TBL
|
|
JOIN blocklisthash BH ON (BH.hash = TBL.blocklisthash)
|
|
JOIN block BK ON (BK.Hash = TBL.BlockHash)
|
|
LEFT OUTER JOIN BlocksetEntry BE ON (BE.BlockSetID = BH.BlocksetID AND BE.""Index"" = (BH.""Index"" * {blocksize / hashsize})+TBL.""Index"")
|
|
WHERE
|
|
BE.BlockSetID IS NULL ");
|
|
if (!hashOnly)
|
|
{
|
|
insertBlocksetEntriesCommand += FormatInvariant($@" UNION
|
|
SELECT BS.ID AS BlocksetID, 0 AS ""Index"", BL.ID AS BlockID
|
|
FROM {m_tempsmalllist} TS
|
|
JOIN Blockset BS ON (BS.FullHash = TS.FileHash AND
|
|
BS.Length = TS.BlockSize AND
|
|
BS.Length <= {blocksize})
|
|
JOIN Block BL ON (BL.Hash = TS.BlockHash AND
|
|
BL.Size = TS.BlockSize)
|
|
LEFT OUTER JOIN BlocksetEntry BE ON (BE.BlocksetID = BS.ID AND BE.""Index"" = 0)
|
|
WHERE
|
|
BE.BlocksetID IS NULL ");
|
|
}
|
|
|
|
try
|
|
{
|
|
// Insert discovered new blocks into block table with volumeid = -1
|
|
cmd.ExecuteNonQuery(insertBlocksCommand);
|
|
// Insert corresponding entries into blockset
|
|
cmd.ExecuteNonQuery(insertBlocksetEntriesCommand);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.Log.WriteErrorMessage(LOGTAG, "BlockOrBlocksetInsertFailed", ex, "Block or Blockset insert failed, committing temporary tables for trace purposes");
|
|
|
|
using (var fixcmd = m_connection.CreateCommand())
|
|
{
|
|
fixcmd.ExecuteNonQuery(FormatInvariant($@"CREATE TABLE ""{m_tempblocklist}_Failure"" AS SELECT * FROM ""{m_tempblocklist}"" "));
|
|
fixcmd.ExecuteNonQuery(FormatInvariant($@"CREATE TABLE ""{m_tempsmalllist}_Failure"" AS SELECT * FROM ""{m_tempsmalllist}"" "));
|
|
}
|
|
|
|
throw new Exception("The recreate failed, please create a bug-report from this database and send it to the developers for further analysis");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddDirectoryEntry(long filesetid, long pathprefixid, string path, DateTime time, long metadataid, IDbTransaction transaction)
|
|
{
|
|
AddEntry(filesetid, pathprefixid, path, time, FOLDER_BLOCKSET_ID, metadataid, transaction);
|
|
}
|
|
|
|
public void AddSymlinkEntry(long filesetid, long pathprefixid, string path, DateTime time, long metadataid, IDbTransaction transaction)
|
|
{
|
|
AddEntry(filesetid, pathprefixid, path, time, SYMLINK_BLOCKSET_ID, metadataid, transaction);
|
|
}
|
|
|
|
public void AddFileEntry(long filesetid, long pathprefixid, string path, DateTime time, long blocksetid, long metadataid, IDbTransaction transaction)
|
|
{
|
|
AddEntry(filesetid, pathprefixid, path, time, blocksetid, metadataid, transaction);
|
|
}
|
|
|
|
private void AddEntry(long filesetid, long pathprefixid, string path, DateTime time, long blocksetid, long metadataid, IDbTransaction transaction)
|
|
{
|
|
m_findFilesetCommand.Transaction = transaction;
|
|
m_findFilesetCommand.SetParameterValue(0, pathprefixid);
|
|
m_findFilesetCommand.SetParameterValue(1, path);
|
|
m_findFilesetCommand.SetParameterValue(2, blocksetid);
|
|
m_findFilesetCommand.SetParameterValue(3, metadataid);
|
|
var fileid = m_findFilesetCommand.ExecuteScalarInt64(-1);
|
|
|
|
if (fileid < 0)
|
|
{
|
|
m_insertFileCommand.Transaction = transaction;
|
|
m_insertFileCommand.SetParameterValue(0, pathprefixid);
|
|
m_insertFileCommand.SetParameterValue(1, path);
|
|
m_insertFileCommand.SetParameterValue(2, blocksetid);
|
|
m_insertFileCommand.SetParameterValue(3, metadataid);
|
|
fileid = m_insertFileCommand.ExecuteScalarInt64(-1);
|
|
}
|
|
|
|
m_insertFilesetEntryCommand.Transaction = transaction;
|
|
m_insertFilesetEntryCommand.SetParameterValue(0, filesetid);
|
|
m_insertFilesetEntryCommand.SetParameterValue(1, fileid);
|
|
m_insertFilesetEntryCommand.SetParameterValue(2, time.ToUniversalTime().Ticks);
|
|
m_insertFilesetEntryCommand.ExecuteNonQuery();
|
|
}
|
|
|
|
public long AddMetadataset(string metahash, long metahashsize, IEnumerable<string> metablocklisthashes, long expectedmetablocklisthashes, IDbTransaction transaction)
|
|
{
|
|
var metadataid = -1L;
|
|
if (metahash == null)
|
|
return metadataid;
|
|
|
|
m_findMetadatasetCommand.Transaction = transaction;
|
|
m_findMetadatasetCommand.SetParameterValue(0, metahash);
|
|
m_findMetadatasetCommand.SetParameterValue(1, metahashsize);
|
|
metadataid = m_findMetadatasetCommand.ExecuteScalarInt64(-1);
|
|
if (metadataid != -1)
|
|
return metadataid;
|
|
|
|
var blocksetid = AddBlockset(metahash, metahashsize, metablocklisthashes, expectedmetablocklisthashes, transaction);
|
|
|
|
m_insertMetadatasetCommand.Transaction = transaction;
|
|
m_insertMetadatasetCommand.SetParameterValue(0, blocksetid);
|
|
metadataid = m_insertMetadatasetCommand.ExecuteScalarInt64(-1);
|
|
|
|
return metadataid;
|
|
}
|
|
|
|
public long AddBlockset(string fullhash, long size, IEnumerable<string> blocklisthashes, long expectedblocklisthashes, IDbTransaction transaction)
|
|
{
|
|
m_findBlocksetCommand.Transaction = transaction;
|
|
m_findBlocksetCommand.SetParameterValue(0, size);
|
|
m_findBlocksetCommand.SetParameterValue(1, fullhash);
|
|
var blocksetid = m_findBlocksetCommand.ExecuteScalarInt64(-1);
|
|
if (blocksetid != -1)
|
|
return blocksetid;
|
|
|
|
m_insertBlocksetCommand.Transaction = transaction;
|
|
m_insertBlocksetCommand.SetParameterValue(0, size);
|
|
m_insertBlocksetCommand.SetParameterValue(1, fullhash);
|
|
blocksetid = m_insertBlocksetCommand.ExecuteScalarInt64(-1);
|
|
|
|
long c = 0;
|
|
if (blocklisthashes != null)
|
|
{
|
|
var index = 0L;
|
|
m_insertBlocklistHashCommand.Transaction = transaction;
|
|
m_insertBlocklistHashCommand.SetParameterValue(0, blocksetid);
|
|
|
|
foreach (var hash in blocklisthashes)
|
|
{
|
|
if (!string.IsNullOrEmpty(hash))
|
|
{
|
|
c++;
|
|
if (c <= expectedblocklisthashes)
|
|
{
|
|
m_insertBlocklistHashCommand.SetParameterValue(1, index++);
|
|
m_insertBlocklistHashCommand.SetParameterValue(2, hash);
|
|
m_insertBlocklistHashCommand.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (c != expectedblocklisthashes)
|
|
Logging.Log.WriteWarningMessage(LOGTAG, "MismatchInBlocklistHashCount", null, "Mismatching number of blocklist hashes detected on blockset {2}. Expected {0} blocklist hashes, but found {1}", expectedblocklisthashes, c, blocksetid);
|
|
|
|
return blocksetid;
|
|
}
|
|
|
|
public bool UpdateBlock(string hash, long size, long volumeID, IDbTransaction transaction, ref bool anyChange)
|
|
{
|
|
m_findHashBlockCommand.Transaction = transaction;
|
|
m_findHashBlockCommand.SetParameterValue(0, hash);
|
|
m_findHashBlockCommand.SetParameterValue(1, size);
|
|
var currentVolumeId = m_findHashBlockCommand.ExecuteScalarInt64(-2);
|
|
|
|
if (currentVolumeId == volumeID)
|
|
return false;
|
|
|
|
anyChange = true;
|
|
|
|
if (currentVolumeId == -2)
|
|
{
|
|
//Insert
|
|
m_insertBlockCommand.Transaction = transaction;
|
|
m_insertBlockCommand.SetParameterValue(0, hash);
|
|
m_insertBlockCommand.SetParameterValue(1, size);
|
|
m_insertBlockCommand.SetParameterValue(2, volumeID);
|
|
m_insertBlockCommand.ExecuteNonQuery();
|
|
|
|
return true;
|
|
}
|
|
else if (currentVolumeId == -1)
|
|
{
|
|
//Update
|
|
m_updateBlockVolumeCommand.Transaction = transaction;
|
|
m_updateBlockVolumeCommand.SetParameterValue(0, volumeID);
|
|
m_updateBlockVolumeCommand.SetParameterValue(1, hash);
|
|
m_updateBlockVolumeCommand.SetParameterValue(2, size);
|
|
var c = m_updateBlockVolumeCommand.ExecuteNonQuery();
|
|
if (c != 1)
|
|
throw new Exception($"Failed to update table, found {c} entries for key {hash} with size {size}");
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
m_insertDuplicateBlockCommand.Transaction = transaction;
|
|
m_insertDuplicateBlockCommand.SetParameterValue(0, hash);
|
|
m_insertDuplicateBlockCommand.SetParameterValue(1, size);
|
|
m_insertDuplicateBlockCommand.SetParameterValue(2, volumeID);
|
|
m_insertDuplicateBlockCommand.ExecuteNonQuery();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void AddSmallBlocksetLink(string filehash, string blockhash, long blocksize, IDbTransaction transaction)
|
|
{
|
|
m_insertSmallBlockset.Transaction = transaction;
|
|
m_insertSmallBlockset.SetParameterValue(0, filehash);
|
|
m_insertSmallBlockset.SetParameterValue(1, blockhash);
|
|
m_insertSmallBlockset.SetParameterValue(2, blocksize);
|
|
m_insertSmallBlockset.ExecuteNonQuery();
|
|
}
|
|
|
|
public bool AddTempBlockListHash(string hash, IEnumerable<string> blocklisthashes, IDbTransaction transaction)
|
|
{
|
|
m_findTempBlockListHashCommand.Transaction = transaction;
|
|
m_findTempBlockListHashCommand.SetParameterValue(0, hash);
|
|
var r = m_findTempBlockListHashCommand.ExecuteScalar();
|
|
if (r != null && r != DBNull.Value)
|
|
return false;
|
|
|
|
m_insertTempBlockListHash.Transaction = transaction;
|
|
m_insertTempBlockListHash.SetParameterValue(0, hash);
|
|
|
|
var index = 0L;
|
|
|
|
foreach (var s in blocklisthashes)
|
|
{
|
|
m_insertTempBlockListHash.SetParameterValue(1, s);
|
|
m_insertTempBlockListHash.SetParameterValue(2, index++);
|
|
m_insertTempBlockListHash.ExecuteNonQuery();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
public IEnumerable<string> GetBlockLists(long volumeid)
|
|
{
|
|
using (var cmd = m_connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = @"SELECT DISTINCT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"", ""Block"" WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash"" AND ""Block"".""VolumeID"" = ?";
|
|
cmd.AddParameter(volumeid);
|
|
|
|
using (var rd = cmd.ExecuteReader())
|
|
while (rd.Read())
|
|
yield return rd.GetValue(0).ToString();
|
|
}
|
|
}
|
|
|
|
public IEnumerable<IRemoteVolume> GetMissingBlockListVolumes(int passNo, long blocksize, long hashsize, bool forceBlockUse)
|
|
{
|
|
using (var cmd = m_connection.CreateCommand())
|
|
{
|
|
var selectCommand = @"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"", ""RemoteVolume"".""ID"" FROM ""RemoteVolume""";
|
|
|
|
var missingBlocklistEntries =
|
|
FormatInvariant($@"SELECT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"" LEFT OUTER JOIN ""BlocksetEntry"" ON ""BlocksetEntry"".""Index"" = (""BlocklistHash"".""Index"" * {blocksize / hashsize}) AND ""BlocksetEntry"".""BlocksetID"" = ""BlocklistHash"".""BlocksetID"" WHERE ""BlocksetEntry"".""BlocksetID"" IS NULL");
|
|
|
|
var missingBlockInfo =
|
|
@"SELECT ""VolumeID"" FROM ""Block"" WHERE ""VolumeID"" < 0 AND SIZE > 0";
|
|
|
|
var missingBlocklistVolumes = FormatInvariant(
|
|
$@"SELECT ""VolumeID"" FROM ""Block"", ({missingBlocklistEntries}) A WHERE ""A"".""Hash"" = ""Block"".""Hash"" "
|
|
);
|
|
|
|
var countMissingInformation = FormatInvariant($@"SELECT COUNT(*) FROM (SELECT DISTINCT ""VolumeID"" FROM ({missingBlockInfo} UNION {missingBlocklistVolumes}))");
|
|
|
|
if (passNo == 0)
|
|
{
|
|
// On the first pass, we select all the volumes we know we need,
|
|
// which may be an empty list
|
|
cmd.CommandText = selectCommand + FormatInvariant($@" WHERE ""ID"" IN ({missingBlocklistVolumes})");
|
|
|
|
// Reset the list
|
|
m_proccessedVolumes = new Dictionary<long, long>();
|
|
}
|
|
else
|
|
{
|
|
//On anything but the first pass, we check if we are done
|
|
var r = cmd.ExecuteScalarInt64(countMissingInformation, 0);
|
|
if (r == 0 && !forceBlockUse)
|
|
yield break;
|
|
|
|
if (passNo == 1)
|
|
{
|
|
// On the second pass, we select all volumes that are not mentioned in the db
|
|
|
|
var mentionedVolumes =
|
|
@"SELECT DISTINCT ""VolumeID"" FROM ""Block"" ";
|
|
|
|
cmd.CommandText = selectCommand + FormatInvariant($@" WHERE ""ID"" NOT IN ({mentionedVolumes}) AND ""Type"" = ? ");
|
|
cmd.AddParameter(RemoteVolumeType.Blocks.ToString());
|
|
}
|
|
else
|
|
{
|
|
// On the final pass, we select all volumes
|
|
// the filter will ensure that we do not download anything twice
|
|
cmd.CommandText = selectCommand + @" WHERE ""Type"" = ?";
|
|
cmd.AddParameter(RemoteVolumeType.Blocks.ToString());
|
|
}
|
|
}
|
|
|
|
using (var rd = cmd.ExecuteReader())
|
|
{
|
|
while (rd.Read())
|
|
{
|
|
var volumeID = rd.GetInt64(3);
|
|
|
|
// Guard against multiple downloads of the same file
|
|
if (!m_proccessedVolumes.ContainsKey(volumeID))
|
|
{
|
|
m_proccessedVolumes.Add(volumeID, volumeID);
|
|
|
|
yield return new RemoteVolume(
|
|
rd.GetString(0),
|
|
rd.ConvertValueToString(1),
|
|
rd.ConvertValueToInt64(2, -1)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CleanupMissingVolumes()
|
|
{
|
|
var tablename = "SwapBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
|
|
|
|
// TODO: either hardcode all string constants or none
|
|
|
|
// The first part of this query swaps out blocks for non-present remote files with
|
|
// existing ones (as recorded in the DuplicateBlock table)
|
|
// The second part removes references to the non-present remote files,
|
|
// and marks the index files that pointed to them, such that they will be removed later on
|
|
var sql = FormatInvariant($@"
|
|
CREATE TEMPORARY TABLE ""{tablename}"" AS
|
|
SELECT
|
|
""A"".""ID"" AS ""BlockID"", ""A"".""VolumeID"" AS ""SourceVolumeID"", ""A"".""State"" AS ""SourceVolumeState"", ""B"".""VolumeID"" AS ""TargetVolumeID"", ""B"".""State"" AS ""TargetVolumeState""
|
|
FROM
|
|
(SELECT
|
|
""Block"".""ID"", ""Block"".""VolumeID"", ""Remotevolume"".""State""
|
|
FROM ""Block"", ""Remotevolume""
|
|
WHERE ""Block"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""State"" = '{RemoteVolumeState.Temporary}'
|
|
) A,
|
|
(SELECT
|
|
""DuplicateBlock"".""BlockID"", MIN(""DuplicateBlock"".""VolumeID"") AS ""VolumeID"", ""Remotevolume"".""State""
|
|
FROM ""DuplicateBlock"", ""Remotevolume""
|
|
WHERE ""DuplicateBlock"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""State"" = '{RemoteVolumeState.Verified}'
|
|
GROUP BY ""DuplicateBlock"".""BlockID"", ""Remotevolume"".""State""
|
|
) B
|
|
WHERE ""A"".""ID"" = ""B"".""BlockID"";
|
|
|
|
UPDATE ""Block""
|
|
SET ""VolumeID"" = (SELECT
|
|
""TargetVolumeID"" FROM ""{tablename}""
|
|
WHERE ""Block"".""ID"" = ""{tablename}"".""BlockID""
|
|
)
|
|
WHERE ""Block"".""ID"" IN (SELECT ""BlockID"" FROM ""{tablename}"");
|
|
|
|
UPDATE ""DuplicateBlock""
|
|
SET ""VolumeID"" = (SELECT
|
|
""SourceVolumeID"" FROM ""{tablename}""
|
|
WHERE ""DuplicateBlock"".""BlockID"" = ""{tablename}"".""BlockID""
|
|
)
|
|
WHERE
|
|
(""DuplicateBlock"".""BlockID"", ""DuplicateBlock"".""VolumeID"")
|
|
IN (SELECT ""BlockID"", ""TargetVolumeID"" FROM ""{tablename}"");
|
|
|
|
DROP TABLE ""{tablename}"";
|
|
|
|
DELETE FROM ""IndexBlockLink"" WHERE ""BlockVolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = '{RemoteVolumeType.Blocks}' AND ""State"" = '{RemoteVolumeState.Temporary}' AND ""ID"" NOT IN (SELECT DISTINCT ""VolumeID"" FROM ""Block""));
|
|
DELETE FROM ""DuplicateBlock"" WHERE ""VolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = '{RemoteVolumeType.Blocks}' AND ""State"" = '{RemoteVolumeState.Temporary}' AND ""ID"" NOT IN (SELECT DISTINCT ""VolumeID"" FROM ""Block""));
|
|
DELETE FROM ""RemoteVolume"" WHERE ""Type"" = '{RemoteVolumeType.Blocks}' AND ""State"" = '{RemoteVolumeState.Temporary}' AND ""ID"" NOT IN (SELECT DISTINCT ""VolumeID"" FROM ""Block"");
|
|
");
|
|
|
|
// We could delete these, but we don't have to, so we keep them around until the next compact is done
|
|
// UPDATE ""RemoteVolume"" SET ""State"" = ""{3}"" WHERE ""Type"" = ""{5}"" AND ""ID"" NOT IN (SELECT ""IndexVolumeID"" FROM ""IndexBlockLink"");
|
|
|
|
var countsql = FormatInvariant($@"SELECT COUNT(*) FROM ""RemoteVolume"" WHERE ""State"" = '{RemoteVolumeState.Temporary}' AND ""Type"" = '{RemoteVolumeType.Blocks}' ");
|
|
|
|
using (var cmd = m_connection.CreateCommand())
|
|
{
|
|
var cnt = cmd.ExecuteScalarInt64(countsql);
|
|
if (cnt > 0)
|
|
{
|
|
Logging.Log.WriteWarningMessage(LOGTAG, "MissingVolumesDetected", null, "Found {0} missing volumes; attempting to replace blocks from existing volumes", cnt);
|
|
cmd.ExecuteNonQuery(sql);
|
|
|
|
var cnt2 = cmd.ExecuteScalarInt64(countsql);
|
|
Logging.Log.WriteVerboseMessage(LOGTAG, "ReplacedMissingVolumes", "Replaced blocks for {0} missing volumes; there are now {1} missing volumes", cnt, cnt2);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move blocks that are not referenced by any files to DeletedBlock table.
|
|
/// </summary>
|
|
/// Needs to be called after the last FindMissingBlocklistHashes, otherwise the tables are not up to date.
|
|
public void CleanupDeletedBlocks(IDbTransaction transaction)
|
|
{
|
|
// Find out which blocks are deleted and move them into DeletedBlock, so that compact notices these blocks are empty
|
|
// Deleted blocks do not appear in the BlocksetEntry and not in the BlocklistHash table
|
|
|
|
var tmptablename = "DeletedBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
|
|
|
|
using (var tr = new TemporaryTransactionWrapper(m_connection, transaction))
|
|
using (var cmd = m_connection.CreateCommand(tr.Parent))
|
|
{
|
|
// 1. Select blocks not used by any file and not as a blocklist into temporary table
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"CREATE TEMPORARY TABLE ""{tmptablename}""
|
|
AS SELECT ""Block"".""ID"", ""Block"".""Hash"", ""Block"".""Size"", ""Block"".""VolumeID"" FROM ""Block""
|
|
WHERE ""Block"".""ID"" NOT IN (SELECT ""BlocksetEntry"".""BlockID"" FROM ""BlocksetEntry"")
|
|
AND ""Block"".""Hash"" NOT IN (SELECT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"")"));
|
|
// 2. Insert blocks into DeletedBlock table
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"INSERT INTO ""DeletedBlock"" (""Hash"", ""Size"", ""VolumeID"") SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""{tmptablename}"""));
|
|
// 3. Remove blocks from Block table
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"DELETE FROM ""Block"" WHERE ""ID"" IN (SELECT ""ID"" FROM ""{tmptablename}"")"));
|
|
cmd.ExecuteNonQuery(FormatInvariant($@"DROP TABLE IF EXISTS ""{tmptablename}"""));
|
|
tr.Commit();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
using (var cmd = m_connection.CreateCommand())
|
|
{
|
|
if (m_tempblocklist != null)
|
|
try
|
|
{
|
|
cmd.CommandText = FormatInvariant(@$"DROP TABLE IF EXISTS ""{m_tempblocklist}""");
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.Log.WriteWarningMessage(LOGTAG, "FailedToDropTempBlocklist", ex, "Failed to drop temporary blocklist table {0}", m_tempblocklist);
|
|
}
|
|
finally
|
|
{
|
|
m_tempblocklist = null;
|
|
}
|
|
|
|
if (m_tempsmalllist != null)
|
|
try
|
|
{
|
|
cmd.CommandText = FormatInvariant(@$"DROP TABLE IF EXISTS ""{m_tempsmalllist}""");
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.Log.WriteWarningMessage(LOGTAG, "FailedToDropTempSmalllist", ex, "Failed to drop temporary smalllist table {0}", m_tempsmalllist);
|
|
}
|
|
finally
|
|
{
|
|
m_tempsmalllist = null;
|
|
}
|
|
|
|
}
|
|
|
|
base.Dispose();
|
|
}
|
|
}
|
|
}
|