2025-01-28 08:54:50 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-02-25 17:41:57 +01:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-01-28 08:54:50 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-02-25 17:41:57 +01:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-28 08:54:50 +01:00
// all copies or substantial portions of the Software.
2025-02-25 17:41:57 +01:00
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-04-15 08:24:01 +02:00
// DEALINGS IN THE SOFTWARE.
2025-04-03 15:46:20 +02:00
#nullable enable
2024-02-28 15:45:30 +01:00
using System ;
2025-02-25 17:42:30 +01:00
using System.Collections.Concurrent ;
2013-03-08 22:24:54 +01:00
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
2025-05-13 08:33:09 +02:00
using System.Threading.Tasks ;
2019-09-01 09:47:04 -07:00
using Duplicati.Library.Common.IO ;
2024-12-03 08:35:14 +01:00
using Duplicati.Library.Main.Operation.Restore ;
2013-05-08 20:17:07 +02:00
using Duplicati.Library.Main.Volumes ;
2025-05-16 15:49:40 +02:00
using Duplicati.Library.SQLiteHelper ;
2019-09-29 20:16:28 -07:00
using Duplicati.Library.Utility ;
2025-05-13 08:33:09 +02:00
using Microsoft.Data.Sqlite ;
2013-03-08 22:24:54 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Database
2013-03-08 22:24:54 +01:00
{
2018-10-13 16:53:28 -07:00
internal class LocalRestoreDatabase : LocalDatabase
2013-03-08 22:24:54 +01:00
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( LocalRestoreDatabase ));
2019-10-19 10:56:21 -07:00
protected readonly string m_temptabsetguid = Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2024-11-22 15:26:04 +01:00
/// <summary>
/// The name of the temporary table in the database, which is used to store the list of files to restore.
/// </summary>
2025-04-03 15:46:20 +02:00
protected string? m_tempfiletable ;
protected string? m_tempblocktable ;
2025-05-23 15:46:30 +02:00
protected ConcurrentBag <( SqliteConnection , ReusableTransaction )> m_connection_pool = [];
2025-04-03 15:46:20 +02:00
protected string? m_latestblocktable ;
protected string? m_fileprogtable ;
protected string? m_totalprogtable ;
protected string? m_filesnewlydonetable ;
2016-02-27 22:04:48 +01:00
2013-05-20 13:48:44 +02:00
protected DateTime m_restoreTime ;
2024-11-08 05:56:48 +01:00
public DateTime RestoreTime { get { return m_restoreTime ; } }
2013-03-08 22:24:54 +01:00
2025-05-21 08:46:18 +02:00
public static async Task < LocalRestoreDatabase > CreateAsync ( string path , long pagecachesize , LocalRestoreDatabase ? dbnew = null )
2013-03-08 22:24:54 +01:00
{
2025-05-21 08:46:18 +02:00
dbnew ??= new LocalRestoreDatabase ();
2025-05-16 13:44:57 +02:00
2025-05-21 08:46:18 +02:00
dbnew = ( LocalRestoreDatabase ) await CreateLocalDatabaseAsync ( path , "Restore" , false , pagecachesize , dbnew );
dbnew . ShouldCloseConnection = true ;
2025-05-16 13:44:57 +02:00
2025-05-21 08:46:18 +02:00
return dbnew ;
2013-03-08 22:24:54 +01:00
}
2025-05-21 08:46:18 +02:00
public static async Task < LocalRestoreDatabase > CreateAsync ( LocalDatabase dbparent , LocalRestoreDatabase ? dbnew = null )
2013-03-08 22:24:54 +01:00
{
2025-05-21 08:46:18 +02:00
dbnew ??= new LocalRestoreDatabase ();
2025-05-16 13:44:57 +02:00
return ( LocalRestoreDatabase ) await CreateLocalDatabaseAsync ( dbparent , dbnew );
2013-03-08 22:24:54 +01:00
}
2016-02-27 22:04:48 +01:00
/// <summary>
/// Create tables and triggers for automatic tracking of progress during a restore operation.
/// This replaces continuous requerying of block progress by iterating over blocks table.
/// SQLite is much faster keeping information up to date with internal triggers.
/// </summary>
/// <param name="createFilesNewlyDoneTracker"> This allows to create another table that keeps track
/// of all files that are done (all data blocks restored). </param>
/// <remarks>
/// The method is prepared to create a table that keeps track of all files being done completely.
/// That means, it fires for files where the number of restored blocks equals the number of all blocks.
/// It is intended to be used for fast identification of fully restored files to trigger their verification.
/// It should be read after a commit and truncated after putting the files to a verification queue.
/// Note: If a file is done once and then set back to a none restored state, the file is not automatically removed.
2019-11-30 11:35:43 -08:00
/// But if it reaches a restored state later, it will be re-added (trigger will fire)
2016-02-27 22:04:48 +01:00
/// </remarks>
2025-05-13 08:33:09 +02:00
public async Task CreateProgressTracker ( bool createFilesNewlyDoneTracker )
2016-02-27 22:04:48 +01:00
{
2025-03-14 14:34:56 +01:00
m_fileprogtable = "FileProgress-" + m_temptabsetguid ;
m_totalprogtable = "TotalProgress-" + m_temptabsetguid ;
m_filesnewlydonetable = createFilesNewlyDoneTracker ? "FilesNewlyDone-" + m_temptabsetguid : null ;
2016-02-27 22:04:48 +01:00
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ()
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-13 08:33:09 +02:00
// How to handle METADATA?
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_fileprogtable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_fileprogtable}"" (
""FileId"" INTEGER PRIMARY KEY,
""TotalBlocks"" INTEGER NOT NULL,
""TotalSize"" INTEGER NOT NULL,
""BlocksRestored"" INTEGER NOT NULL,
""SizeRestored"" INTEGER NOT NULL
)
" );
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_totalprogtable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_totalprogtable}"" (
""TotalFiles"" INTEGER NOT NULL,
""TotalBlocks"" INTEGER NOT NULL,
""TotalSize"" INTEGER NOT NULL,
""FilesFullyRestored"" INTEGER NOT NULL,
""FilesPartiallyRestored"" INTEGER NOT NULL,
""BlocksRestored"" INTEGER NOT NULL,
""SizeRestored"" INTEGER NOT NULL
)
" );
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
if ( createFilesNewlyDoneTracker )
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_filesnewlydonetable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_filesnewlydonetable}"" (
""ID"" INTEGER PRIMARY KEY
)
" );
2025-05-13 08:33:09 +02:00
}
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
try
{
// Initialize statistics with File- and Block-Data (it is valid to already have restored blocks in files)
// A rebuild with this function should be valid anytime.
// Note: FilesNewlyDone is NOT initialized, as in initialization nothing is really new.
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// We use a LEFT JOIN to allow for empty files (no data Blocks)
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// Will be one row per file.
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
INSERT INTO ""{m_fileprogtable}"" (
""FileId"",
""TotalBlocks"",
""TotalSize"",
""BlocksRestored"",
""SizeRestored""
)
SELECT
""F"".""ID"",
IFNULL(COUNT(""B"".""ID""), 0),
IFNULL(SUM(""B"".""Size""), 0),
2025-05-20 06:07:45 +02:00
IFNULL(
COUNT(
CASE ""B"".""Restored""
WHEN 1
THEN ""B"".""ID""
ELSE NULL
END
),
0
),
IFNULL(
SUM(
CASE ""B"".""Restored""
WHEN 1
THEN ""B"".""Size""
ELSE 0
END
),
0
)
2025-05-16 15:49:40 +02:00
FROM ""{m_tempfiletable}"" ""F""
LEFT JOIN ""{m_tempblocktable}"" ""B""
ON ""B"".""FileID"" = ""F"".""ID""
WHERE ""B"".""Metadata"" IS NOT 1
GROUP BY ""F"".""ID""
" );
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// Will result in a single line (no support to also track metadata)
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
INSERT INTO ""{m_totalprogtable}"" (
""TotalFiles"",
""TotalBlocks"",
""TotalSize"",
""FilesFullyRestored"",
""FilesPartiallyRestored"",
""BlocksRestored"",
""SizeRestored""
)
SELECT
IFNULL(COUNT(""P"".""FileId""), 0),
IFNULL(SUM(""P"".""TotalBlocks""), 0),
IFNULL(SUM(""P"".""TotalSize""), 0),
2025-05-20 06:07:45 +02:00
IFNULL(
COUNT(
CASE
WHEN ""P"".""BlocksRestored"" = ""P"".""TotalBlocks""
THEN 1
ELSE NULL
END
),
0
),
IFNULL(
COUNT(
CASE
WHEN
""P"".""BlocksRestored"" BETWEEN 1
AND ""P"".""TotalBlocks"" - 1
THEN 1
ELSE NULL
END
),
0
),
IFNULL(SUM(""P"".""BlocksRestored""), 0),
IFNULL(SUM(""P"".""SizeRestored""), 0)
2025-05-16 15:49:40 +02:00
FROM ""{m_fileprogtable}"" ""P""
" );
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// Finally we create TRIGGERs to keep all our statistics up to date.
// This is lightning fast, as SQLite uses internal hooks and our indices to do the update magic.
// Note: We do assume that neither files nor blocks will be added or deleted during restore process
// and that the size of each block stays constant so there is no need to track that information
// with additional INSERT and DELETE triggers.
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// A trigger to update the file-stat entry each time a block changes restoration state.
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TRIGGER ""TrackRestoredBlocks_{m_tempblocktable}""
AFTER UPDATE OF ""Restored""
ON ""{m_tempblocktable}""
WHEN OLD.""Restored"" != NEW.""Restored""
AND NEW.""Metadata"" = 0
BEGIN UPDATE ""{m_fileprogtable}""
SET
2025-05-20 06:07:45 +02:00
""BlocksRestored"" =
""{m_fileprogtable}"".""BlocksRestored""
+ (NEW.""Restored"" - OLD.""Restored""),
""SizeRestored"" =
""{m_fileprogtable}"".""SizeRestored""
+ ((NEW.""Restored"" - OLD.""Restored"") * NEW.Size)
2025-05-16 15:49:40 +02:00
WHERE ""{m_fileprogtable}"".""FileId"" = NEW.""FileID""
; END
" );
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
// A trigger to update total stats each time a file stat changed (nested triggering by file-stats)
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TRIGGER ""UpdateTotalStats_{m_fileprogtable}""
AFTER UPDATE ON ""{m_fileprogtable}""
BEGIN UPDATE ""{m_totalprogtable}""
SET
2025-05-20 06:07:45 +02:00
""FilesFullyRestored"" =
""{m_totalprogtable}"".""FilesFullyRestored""
+ (CASE
WHEN NEW.""BlocksRestored"" = NEW.""TotalBlocks""
THEN 1
ELSE 0
END)
- (CASE
WHEN OLD.""BlocksRestored"" = OLD.""TotalBlocks""
THEN 1
ELSE 0
END),
""FilesPartiallyRestored"" =
""{m_totalprogtable}"".""FilesPartiallyRestored""
+ (CASE
WHEN
NEW.""BlocksRestored"" BETWEEN 1
AND NEW.""TotalBlocks"" - 1
THEN 1
ELSE 0
END)
- (CASE
WHEN
OLD.""BlocksRestored"" BETWEEN 1
AND OLD.""TotalBlocks"" - 1
THEN 1
ELSE 0
END),
""BlocksRestored"" =
""{m_totalprogtable}"".""BlocksRestored""
+ NEW.""BlocksRestored""
- OLD.""BlocksRestored"",
""SizeRestored"" =
""{m_totalprogtable}"".""SizeRestored""
+ NEW.""SizeRestored""
- OLD.""SizeRestored""
2025-05-16 15:49:40 +02:00
; END
" );
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
if ( createFilesNewlyDoneTracker )
{
// A trigger checking if a file is done (all blocks restored in file-stat) (nested triggering by file-stats)
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TRIGGER ""UpdateFilesNewlyDone_{m_fileprogtable}""
AFTER UPDATE OF
""BlocksRestored"",
""TotalBlocks""
ON ""{m_fileprogtable}""
WHEN NEW.""BlocksRestored"" = NEW.""TotalBlocks""
BEGIN
INSERT OR IGNORE INTO ""{m_filesnewlydonetable}"" (""ID"")
VALUES (NEW.""FileId"");
END
" );
2016-02-27 22:04:48 +01:00
}
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex )
{
m_fileprogtable = null ;
m_totalprogtable = null ;
Logging . Log . WriteWarningMessage ( LOGTAG , "ProgressTrackerSetupError" , ex , "Failed to set up progress tracking tables" );
throw ;
}
finally
{
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2016-02-27 22:04:48 +01:00
}
}
2025-05-13 08:33:09 +02:00
public async Task < Tuple < long , long >> PrepareRestoreFilelist ( DateTime restoretime , long [] versions , IFilter filter )
2013-05-29 22:15:28 +02:00
{
2022-01-31 14:22:50 +01:00
m_tempfiletable = "Fileset-" + m_temptabsetguid ;
m_tempblocktable = "Blocks-" + m_temptabsetguid ;
2013-03-08 22:24:54 +01:00
2025-01-28 08:54:50 +01:00
using ( var cmd = m_connection . CreateCommand ())
2013-03-08 22:24:54 +01:00
{
2025-05-19 10:49:22 +02:00
cmd . SetTransaction ( m_rtr );
2025-05-22 06:47:38 +02:00
var filesetIds = await GetFilesetIDs ( Library . Utility . Utility . NormalizeDateTime ( restoretime ), versions ). ToListAsync ();
2025-01-28 08:54:50 +01:00
while ( filesetIds . Count > 0 )
2013-03-08 22:24:54 +01:00
{
2013-08-24 22:27:30 +02:00
var filesetId = filesetIds [ 0 ];
filesetIds . RemoveAt ( 0 );
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( @"
SELECT ""Timestamp""
FROM ""Fileset""
WHERE ""ID"" = @FilesetId
" )
. SetParameterValue ( "@FilesetId" , filesetId );
2025-05-13 08:33:09 +02:00
m_restoreTime = ParseFromEpochSeconds ( await cmd . ExecuteScalarInt64Async ( 0 ));
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
var ix = await FilesetTimes ()
. Select (( value , index ) => new { value . Key , index })
2013-08-24 22:27:30 +02:00
. Where ( n => n . Key == filesetId )
. Select ( pair => pair . index + 1 )
2025-05-13 08:33:09 +02:00
. FirstOrDefaultAsync () - 1 ;
2024-11-08 05:56:48 +01:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "SearchingBackup" , "Searching backup {0} ({1}) ..." , ix , m_restoreTime );
2024-11-08 05:56:48 +01:00
2013-08-24 22:27:30 +02:00
cmd . Parameters . Clear ();
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tempfiletable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tempblocktable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_tempfiletable}"" (
""ID"" INTEGER PRIMARY KEY,
""Path"" TEXT NOT NULL,
""BlocksetID"" INTEGER NOT NULL,
""MetadataID"" INTEGER NOT NULL,
""TargetPath"" TEXT NULL,
""DataVerified"" BOOLEAN NOT NULL,
""LatestBlocksetId"" INTEGER,
""LocalSourceExists"" BOOLEAN
)
" );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_tempblocktable}"" (
""ID"" INTEGER PRIMARY KEY,
""FileID"" INTEGER NOT NULL,
""Index"" INTEGER NOT NULL,
""Hash"" TEXT NOT NULL,
""Size"" INTEGER NOT NULL,
""Restored"" BOOLEAN NOT NULL,
""Metadata"" BOOLEAN NOT NULL,
""VolumeID"" INTEGER NOT NULL,
""BlockID"" INTEGER NOT NULL
)
" );
2015-11-16 12:57:34 +01:00
2018-06-14 10:12:24 +02:00
// TODO: Optimize to use the path prefix
2013-08-24 22:27:30 +02:00
if ( filter == null || filter . Empty )
2013-03-08 22:24:54 +01:00
{
2013-08-24 22:27:30 +02:00
// Simple case, restore everything
2025-05-16 15:49:40 +02:00
await cmd . SetCommandAndParameters ( $@"
INSERT INTO ""{m_tempfiletable}"" (
""ID"",
""Path"",
""BlocksetID"",
""MetadataID"",
""DataVerified""
)
SELECT
""File"".""ID"",
""File"".""Path"",
""File"".""BlocksetID"",
""File"".""MetadataID"",
0
FROM
""File"",
""FilesetEntry""
2025-05-19 10:46:50 +02:00
WHERE
""File"".""ID"" = ""FilesetEntry"".""FileID""
AND ""FilesetEntry"".""FilesetID"" = @FilesetId
2025-05-16 15:49:40 +02:00
" )
. SetParameterValue ( "@FilesetId" , filesetId )
. ExecuteNonQueryAsync ();
2013-08-24 22:27:30 +02:00
}
2025-03-19 22:23:57 +01:00
else if ( Library . Utility . Utility . IsFSCaseSensitive && filter is FilterExpression expression && expression . Type == FilterType . Simple )
2013-08-24 22:27:30 +02:00
{
2025-05-13 08:33:09 +02:00
using ( new Logging . Timer ( LOGTAG , "CommitBeforePrepareFileset" , "CommitBeforePrepareFileset" ))
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2025-05-19 10:49:22 +02:00
cmd . SetTransaction ( m_rtr );
2013-08-24 22:27:30 +02:00
// If we get a list of filenames, the lookup table is faster
2015-09-15 19:29:58 +02:00
// unfortunately we cannot do this if the filesystem is case sensitive as
// SQLite only supports ASCII compares
2025-05-13 08:33:09 +02:00
var p = expression . GetSimpleList ();
var m_filenamestable = "Filenames-" + m_temptabsetguid ;
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{m_filenamestable}"" (
""Path"" TEXT NOT NULL
)
" );
cmd . SetCommandAndParameters ( $@"
INSERT INTO ""{m_filenamestable}"" (""Path"")
VALUES (@Path)
" );
2024-11-08 05:56:48 +01:00
2025-05-13 08:33:09 +02:00
foreach ( var s in p )
{
2025-05-16 15:49:40 +02:00
await cmd . SetParameterValue ( "@Path" , s )
. ExecuteNonQueryAsync ();
2025-05-13 08:33:09 +02:00
}
2016-03-05 02:00:28 +01:00
2025-05-16 15:49:40 +02:00
var c = await cmd . SetCommandAndParameters ( $@"
INSERT INTO ""{m_tempfiletable}"" (
""ID"",
""Path"",
""BlocksetID"",
""MetadataID"",
""DataVerified""
)
SELECT
""File"".""ID"",
""File"".""Path"",
""File"".""BlocksetID"",
""File"".""MetadataID"",
0
FROM
""File"",
""FilesetEntry""
2025-05-19 10:46:50 +02:00
WHERE
""File"".""ID"" = ""FilesetEntry"".""FileID""
AND ""FilesetEntry"".""FilesetID"" = @FilesetId
AND ""Path"" IN (
SELECT DISTINCT ""Path""
FROM ""{m_filenamestable}""
)
2025-05-16 15:49:40 +02:00
" )
. SetParameterValue ( "@FilesetId" , filesetId )
. ExecuteNonQueryAsync ();
2024-11-08 05:56:48 +01:00
2025-05-13 08:33:09 +02:00
if ( c != p . Length && c != 0 )
{
var sb = new StringBuilder ();
sb . AppendLine ();
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( $@"
SELECT ""Path""
FROM ""{m_filenamestable}""
WHERE ""Path"" NOT IN (
SELECT ""Path""
FROM ""{m_tempfiletable}""
)
" );
using ( var rd = await cmd . ExecuteReaderAsync ())
2025-05-13 08:33:09 +02:00
while ( await rd . ReadAsync ())
sb . AppendLine ( rd . ConvertValueToString ( 0 ));
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( @"
SELECT ""Timestamp""
FROM ""Fileset""
WHERE ""ID"" = @FilesetId
" )
. SetParameterValue ( "@FilesetId" , filesetId );
2025-05-13 08:33:09 +02:00
var actualrestoretime = ParseFromEpochSeconds ( await cmd . ExecuteScalarInt64Async ( 0 ));
2025-03-19 22:23:57 +01:00
2025-05-13 08:33:09 +02:00
Logging . Log . WriteWarningMessage ( LOGTAG , "FilesNotFoundInBackupList" , null , "{0} File(s) were not found in list of files for backup at {1}, will not be restored: {2}" , p . Length - c , actualrestoretime . ToLocalTime (), sb );
cmd . Parameters . Clear ();
}
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_filenamestable}"" " );
2024-11-08 05:56:48 +01:00
2025-05-13 08:33:09 +02:00
using ( new Logging . Timer ( LOGTAG , "CommitAfterPrepareFileset" , "CommitAfterPrepareFileset" ))
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2025-05-19 10:49:22 +02:00
cmd . SetTransaction ( m_rtr );
2013-03-08 22:24:54 +01:00
}
2013-08-24 22:27:30 +02:00
else
2013-03-08 22:24:54 +01:00
{
2013-08-24 22:27:30 +02:00
// Restore but filter elements based on the filter expression
// If this is too slow, we could add a special handler for wildcard searches too
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( @"
SELECT
""File"".""ID"",
""File"".""Path"",
""File"".""BlocksetID"",
""File"".""MetadataID""
FROM
""File"",
""FilesetEntry""
2025-05-19 10:46:50 +02:00
WHERE
""File"".""ID"" = ""FilesetEntry"".""FileID""
AND ""FilesetID"" = @FilesetId
2025-05-16 15:49:40 +02:00
" )
. SetParameterValue ( "@FilesetId" , filesetId );
2024-11-08 05:56:48 +01:00
2025-02-28 10:27:02 +01:00
object [] values = new object [ 4 ];
2025-05-16 15:49:40 +02:00
using var cmd2 = m_connection . CreateCommand ( $@"
INSERT INTO ""{m_tempfiletable}"" (
""ID"",
""Path"",
""BlocksetID"",
""MetadataID"",
""DataVerified""
)
VALUES (
@ID,
@Path,
@BlocksetID,
@MetadataID,
0
)
" );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
{
rd . GetValues ( values );
if ( values [ 1 ] != null && values [ 1 ] != DBNull . Value && FilterExpression . Matches ( filter , values [ 1 ]. ToString ()))
2025-03-14 14:34:56 +01:00
{
2025-05-16 15:49:40 +02:00
await cmd2 . SetParameterValue ( "@ID" , values [ 0 ])
. SetParameterValue ( "@Path" , values [ 1 ])
. SetParameterValue ( "@BlocksetID" , values [ 2 ])
. SetParameterValue ( "@MetadataID" , values [ 3 ])
. ExecuteNonQueryAsync ();
2025-03-14 14:34:56 +01:00
}
2025-05-13 08:33:09 +02:00
}
2013-03-08 22:24:54 +01:00
}
2022-01-31 14:22:50 +01:00
//creating indexes after insertion is much faster
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempfiletable}_ID""
ON ""{m_tempfiletable}"" (""ID"")
" );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempfiletable}_TargetPath""
ON ""{m_tempfiletable}"" (""TargetPath"")
" );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempfiletable}_Path""
ON ""{m_tempfiletable}"" (""Path"")
" );
cmd . SetCommandAndParameters ( $@"
SELECT
COUNT(DISTINCT ""{m_tempfiletable}"".""Path""),
SUM(""Blockset"".""Length"")
FROM
""{m_tempfiletable}"",
""Blockset""
WHERE ""{m_tempfiletable}"".""BlocksetID"" = ""Blockset"".""ID""
" );
using ( var rd = await cmd . ExecuteReaderAsync ())
2013-08-24 22:27:30 +02:00
{
var filecount = 0L ;
var filesize = 0L ;
2013-08-25 13:16:12 +02:00
2025-05-13 08:33:09 +02:00
if ( await rd . ReadAsync ())
2013-08-25 13:16:12 +02:00
{
2015-01-24 21:59:53 +01:00
filecount = rd . ConvertValueToInt64 ( 0 , 0 );
filesize = rd . ConvertValueToInt64 ( 1 , 0 );
2013-08-25 13:16:12 +02:00
}
2013-08-24 22:27:30 +02:00
if ( filecount > 0 )
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteVerboseMessage ( LOGTAG , "RestoreTargetFileCount" , "Needs to restore {0} files ({1})" , filecount , Library . Utility . Utility . FormatSizeString ( filesize ));
2013-08-24 22:27:30 +02:00
return new Tuple < long , long >( filecount , filesize );
}
2024-11-08 05:56:48 +01:00
}
2013-08-24 22:27:30 +02:00
}
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
2024-11-08 05:56:48 +01:00
2013-08-24 22:27:30 +02:00
return new Tuple < long , long >( 0 , 0 );
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task < string? > GetFirstPath ()
2017-01-06 23:01:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT ""Path""
FROM ""{m_tempfiletable}""
ORDER BY LENGTH(""Path"") DESC
LIMIT 1
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
var v0 = await cmd . ExecuteScalarAsync ();
2025-05-13 08:33:09 +02:00
if ( v0 == null || v0 == DBNull . Value )
return null ;
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2024-11-08 05:56:48 +01:00
2025-05-13 08:33:09 +02:00
return v0 . ToString ();
2017-01-06 23:01:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task < string > GetLargestPrefix ()
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT ""Path""
FROM ""{m_tempfiletable}""
ORDER BY LENGTH(""Path"") DESC
LIMIT 1
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
var v0 = await cmd . ExecuteScalarAsync ();
2025-05-13 08:33:09 +02:00
var maxpath = "" ;
if ( v0 != null && v0 != DBNull . Value )
maxpath = v0 . ToString ()!;
2013-03-08 22:24:54 +01:00
2025-05-13 08:33:09 +02:00
var dirsep = Util . GuessDirSeparator ( maxpath );
2017-01-06 23:01:54 +01:00
2025-05-16 15:49:40 +02:00
var filecount = await cmd . ExecuteScalarInt64Async ( $@"
SELECT COUNT(*)
FROM ""{m_tempfiletable}""
" , - 1 );
2025-05-13 08:33:09 +02:00
var foundfiles = - 1L ;
2013-03-08 22:24:54 +01:00
2025-05-13 08:33:09 +02:00
//TODO: Handle FS case-sensitive?
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( $@"
SELECT COUNT(*)
FROM ""{m_tempfiletable}""
WHERE SUBSTR(""Path"", 1, @PrefixLength) = @Prefix
" );
2013-03-08 22:24:54 +01:00
2025-05-13 08:33:09 +02:00
while ( filecount != foundfiles && maxpath . Length > 0 )
{
var mp = Util . AppendDirSeparator ( maxpath , dirsep );
2025-05-16 15:49:40 +02:00
foundfiles = await cmd . SetParameterValue ( "@PrefixLength" , mp . Length )
. SetParameterValue ( "@Prefix" , mp )
. ExecuteScalarInt64Async (- 1 );
2013-03-08 22:24:54 +01:00
2025-05-13 08:33:09 +02:00
if ( filecount != foundfiles )
{
var oldlen = maxpath . Length ;
2017-01-06 23:01:54 +01:00
2025-05-13 08:33:09 +02:00
var lix = maxpath . LastIndexOf ( dirsep , maxpath . Length - 2 , StringComparison . Ordinal );
maxpath = maxpath . Substring ( 0 , lix + 1 );
if ( string . IsNullOrWhiteSpace ( maxpath ) || maxpath . Length == oldlen )
maxpath = "" ;
2013-03-08 22:24:54 +01:00
}
}
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2025-05-13 08:33:09 +02:00
return maxpath == "" ? "" : Util . AppendDirSeparator ( maxpath , dirsep );
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task SetTargetPaths ( string largest_prefix , string destination )
2016-09-15 11:39:27 +02:00
{
2025-05-13 08:33:09 +02:00
var dirsep = Util . GuessDirSeparator ( string . IsNullOrWhiteSpace ( largest_prefix ) ? await GetFirstPath () : largest_prefix );
2017-01-06 23:01:54 +01:00
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ()
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-13 08:33:09 +02:00
if ( string . IsNullOrEmpty ( destination ))
2016-09-15 11:39:27 +02:00
{
2025-05-13 08:33:09 +02:00
//The string fixing here is meant to provide some non-random
// defaults when restoring cross OS, e.g. backup on Linux, restore on Windows
//This is mostly meaningless, and the user really should use --restore-path
2017-01-06 23:01:54 +01:00
2025-05-13 08:33:09 +02:00
if (( OperatingSystem . IsMacOS () || OperatingSystem . IsLinux ()) && dirsep == "\\" )
{
// For Win -> Linux, we remove the colon from the drive letter, and use the drive letter as root folder
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""Targetpath"" =
CASE
WHEN SUBSTR(""Path"", 2, 1) == ':'
THEN '\\' || SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3)
ELSE ""Path""
END
" );
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""Targetpath"" =
CASE
WHEN SUBSTR(""Path"", 1, 2) == '\\'
THEN '\\' || SUBSTR(""Path"", 2)
ELSE ""Path""
END
" );
2017-01-06 23:01:54 +01:00
2025-05-13 08:33:09 +02:00
}
else if ( OperatingSystem . IsWindows () && dirsep == "/" )
{
// For Linux -> Win, we use the temporary folder's drive as the root path
2025-05-16 15:49:40 +02:00
await cmd . SetCommandAndParameters ( $@"
UPDATE ""{m_tempfiletable}""
SET ""Targetpath"" =
CASE
WHEN SUBSTR(""Path"", 1, 1) == '/'
THEN @Path || SUBSTR(""Path"", 2)
ELSE ""Path""
2025-05-20 06:07:45 +02:00
END
" )
2025-05-16 15:49:40 +02:00
. SetParameterValue ( "@Path" , Util . AppendDirSeparator ( System . IO . Path . GetPathRoot ( Library . Utility . TempFolder . SystemTempPath )). Replace ( "\\" , "/" ))
. ExecuteNonQueryAsync ();
2016-09-15 11:39:27 +02:00
}
else
2024-11-08 05:56:48 +01:00
{
2025-05-13 08:33:09 +02:00
// Same OS, just use the path directly
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""Targetpath"" = ""Path""
" );
2017-01-06 23:01:54 +01:00
}
2025-05-13 08:33:09 +02:00
}
else
{
if ( string . IsNullOrEmpty ( largest_prefix ))
{
//Special case, restoring to new folder, but files are from different drives (no shared root on Windows)
2017-01-06 23:01:54 +01:00
2025-05-13 08:33:09 +02:00
// We use the format <restore path> / <drive letter> / <source path>
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" =
CASE
WHEN SUBSTR(""Path"", 2, 1) == ':'
THEN SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3)
ELSE ""Path""
END
" );
2017-01-06 23:01:54 +01:00
2025-05-13 08:33:09 +02:00
// For UNC paths, we use \\server\folder -> <restore path> / <servername> / <source path>
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" =
CASE
WHEN SUBSTR(""Path"", 1, 2) == '\\'
THEN SUBSTR(""Path"", 2)
ELSE ""TargetPath""
END
" );
2025-05-13 08:33:09 +02:00
}
else
2017-01-06 23:01:54 +01:00
{
2025-05-13 08:33:09 +02:00
largest_prefix = Util . AppendDirSeparator ( largest_prefix , dirsep );
2025-05-16 15:49:40 +02:00
await cmd . SetCommandAndParameters ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" = SUBSTR(""Path"", @PrefixLength)
" )
. SetParameterValue ( "@PrefixLength" , largest_prefix . Length + 1 )
. ExecuteNonQueryAsync ();
2017-01-06 23:01:54 +01:00
}
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
// Cross-os path remapping support
if (( OperatingSystem . IsMacOS () || OperatingSystem . IsLinux ()) && dirsep == "\\" )
// For Win paths on Linux
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" = REPLACE(""TargetPath"", '\', '/')
" );
2025-05-13 08:33:09 +02:00
else if ( OperatingSystem . IsWindows () && dirsep == "/" )
// For Linux paths on Windows
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" = REPLACE(REPLACE(""TargetPath"", '\', '_'), '/', '\')
" );
2025-05-13 08:33:09 +02:00
if (! string . IsNullOrEmpty ( destination ))
{
// Paths are now relative with target-os naming system
// so we prefix them with the target path
2025-05-16 15:49:40 +02:00
await cmd . SetCommandAndParameters ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" = @Destination || ""TargetPath""
" )
. SetParameterValue ( "@Destination" , Util . AppendDirSeparator ( destination ))
. ExecuteNonQueryAsync ();
2025-05-13 08:33:09 +02:00
}
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task FindMissingBlocks ( bool skipMetadata )
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ()
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
var p1 = await cmd . ExecuteNonQueryAsync ( $@"
INSERT INTO ""{m_tempblocktable}"" (
""FileID"",
""Index"",
""Hash"",
""Size"",
""Restored"",
""Metadata"",
""VolumeId"",
""BlockId""
)
SELECT DISTINCT
""{m_tempfiletable}"".""ID"",
""BlocksetEntry"".""Index"",
""Block"".""Hash"",
""Block"".""Size"",
0,
0,
""Block"".""VolumeID"",
""Block"".""ID""
FROM
""{m_tempfiletable}"",
""BlocksetEntry"",
""Block""
2025-05-19 10:46:50 +02:00
WHERE
""{m_tempfiletable}"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
2025-05-16 15:49:40 +02:00
" );
2014-11-05 21:43:08 +01:00
2025-05-13 08:33:09 +02:00
var p2 = 0 ;
if (! skipMetadata )
2025-05-16 15:49:40 +02:00
p2 = await cmd . ExecuteNonQueryAsync ( $@"
INSERT INTO ""{m_tempblocktable}"" (
""FileID"",
""Index"",
""Hash"",
""Size"",
""Restored"",
""Metadata"",
""VolumeId"",
""BlockId""
)
SELECT DISTINCT
""{m_tempfiletable}"".""ID"",
""BlocksetEntry"".""Index"",
""Block"".""Hash"",
""Block"".""Size"",
0,
1,
""Block"".""VolumeID"",
""Block"".""ID""
FROM
""{m_tempfiletable}"",
""BlocksetEntry"",
""Block"",
""Metadataset""
2025-05-19 10:46:50 +02:00
WHERE
""{m_tempfiletable}"".""MetadataID"" = ""Metadataset"".""ID""
AND ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
2025-05-16 15:49:40 +02:00
" );
2014-11-05 21:43:08 +01:00
2025-05-13 08:33:09 +02:00
//creating indexes after insertion is much faster
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempblocktable}_HashSizeIndex""
ON ""{m_tempblocktable}"" (""Hash"", ""Size"")
" );
2025-05-13 08:33:09 +02:00
// better suited to speed up commit on UpdateBlocks
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempblocktable}_FileIdIndexIndex""
ON ""{m_tempblocktable}"" (""FileId"", ""Index"")
" );
var size = await cmd . ExecuteScalarInt64Async ( $@"
SELECT SUM(""Size"")
FROM ""{m_tempblocktable}""
" , 0 );
2025-05-13 08:33:09 +02:00
Logging . Log . WriteVerboseMessage ( LOGTAG , "RestoreSourceSize" , "Restore list contains {0} blocks with a total size of {1}" , p1 + p2 , Library . Utility . Utility . FormatSizeString ( size ));
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task UpdateTargetPath ( long ID , string newname )
2016-09-15 11:39:27 +02:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
UPDATE ""{m_tempfiletable}""
SET ""TargetPath"" = @TargetPath
WHERE ""ID"" = @ID
" );
2025-05-19 10:49:22 +02:00
await cmd . SetTransaction ( m_rtr )
2025-05-16 15:49:40 +02:00
. SetParameterValue ( "@TargetPath" , newname )
. SetParameterValue ( "@ID" , ID )
. ExecuteNonQueryAsync ();
await m_rtr . CommitAsync ();
2016-09-15 11:39:27 +02:00
}
2013-05-20 13:48:44 +02:00
2013-03-08 22:24:54 +01:00
public interface IExistingFileBlock
{
string Hash { get ; }
long Index { get ; }
long Size { get ; }
}
public interface IExistingFile
{
string TargetPath { get ; }
2013-06-01 14:22:10 +02:00
string TargetHash { get ; }
2013-04-27 10:20:15 +02:00
long TargetFileID { get ; }
2013-03-08 22:24:54 +01:00
long Length { get ; }
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IExistingFileBlock > Blocks ();
2013-03-08 22:24:54 +01:00
}
public interface IBlockSource
{
string Path { get ; }
long Offset { get ; }
2014-11-05 21:43:08 +01:00
bool IsMetadata { get ; }
2013-03-08 22:24:54 +01:00
}
public interface IBlockDescriptor
{
string Hash { get ; }
long Size { get ; }
long Offset { get ; }
long Index { get ; }
2014-11-05 21:43:08 +01:00
bool IsMetadata { get ; }
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IBlockSource > BlockSources ();
2013-03-08 22:24:54 +01:00
}
public interface ILocalBlockSource
{
string TargetPath { get ; }
2013-04-27 10:20:15 +02:00
long TargetFileID { get ; }
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IBlockDescriptor > Blocks ();
2013-03-08 22:24:54 +01:00
}
2024-11-22 15:26:04 +01:00
/// <summary>
/// Interface for an object describing a file to restore.
/// </summary>
2013-03-08 22:24:54 +01:00
public interface IFileToRestore
{
string Path { get ; }
string Hash { get ; }
2015-03-04 16:23:44 +01:00
long Length { get ; }
2013-03-08 22:24:54 +01:00
}
public interface IPatchBlock
{
long Offset { get ; }
long Size { get ; }
string Key { get ; }
}
public interface IVolumePatch
{
string Path { get ; }
2013-08-23 22:18:13 +02:00
long FileID { get ; }
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IPatchBlock > Blocks ();
2013-03-08 22:24:54 +01:00
}
2014-11-04 15:34:20 +01:00
private class ExistingFile : IExistingFile
{
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
2014-11-04 15:34:20 +01:00
2025-05-13 08:33:09 +02:00
public ExistingFile ( SqliteDataReader rd ) { m_reader = rd ; HasMore = true ; }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
public string TargetPath { get { return m_reader . ConvertValueToString ( 0 ) ?? "" ; } }
public string TargetHash { get { return m_reader . ConvertValueToString ( 1 ) ?? "" ; } }
2025-01-28 08:54:50 +01:00
public long TargetFileID { get { return m_reader . ConvertValueToInt64 ( 2 ); } }
2014-11-04 15:34:20 +01:00
public long Length { get { return m_reader . ConvertValueToInt64 ( 3 ); } }
public bool HasMore { get ; private set ; }
private class ExistingFileBlock : IExistingFileBlock
{
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
2014-11-04 15:34:20 +01:00
2025-05-13 08:33:09 +02:00
public ExistingFileBlock ( SqliteDataReader rd ) { m_reader = rd ; }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
public string Hash { get { return m_reader . ConvertValueToString ( 4 ) ?? "" ; } }
2014-11-04 15:34:20 +01:00
public long Index { get { return m_reader . ConvertValueToInt64 ( 5 ); } }
public long Size { get { return m_reader . ConvertValueToInt64 ( 6 ); } }
}
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IExistingFileBlock > Blocks ()
2014-11-04 15:34:20 +01:00
{
2025-05-16 14:46:08 +02:00
string p = TargetPath ;
while ( HasMore && p == TargetPath )
2014-11-04 15:34:20 +01:00
{
2025-05-16 14:46:08 +02:00
yield return new ExistingFileBlock ( m_reader );
HasMore = await m_reader . ReadAsync ();
2014-11-04 15:34:20 +01:00
}
}
2025-05-16 14:46:08 +02:00
public static async IAsyncEnumerable < IExistingFile > GetExistingFilesWithBlocks ( LocalDatabase db , string tablename )
2014-11-04 15:34:20 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = db . Connection . CreateCommand ( $@"
SELECT
""{tablename}"".""TargetPath"",
""Blockset"".""FullHash"",
""{tablename}"".""ID"",
""Blockset"".""Length"",
""Block"".""Hash"",
""BlocksetEntry"".""Index"",
""Block"".""Size""
FROM
""{tablename}"",
""Blockset"",
""BlocksetEntry"",
""Block""
2025-05-19 10:46:50 +02:00
WHERE
""{tablename}"".""BlocksetID"" = ""Blockset"".""ID""
AND ""BlocksetEntry"".""BlocksetID"" = ""{tablename}"".""BlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
ORDER BY
""{tablename}"".""TargetPath"",
""BlocksetEntry"".""Index""
2025-05-16 15:49:40 +02:00
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( db . Transaction );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
if ( await rd . ReadAsync ())
{
var more = true ;
while ( more )
2025-03-19 22:23:57 +01:00
{
2025-05-13 08:33:09 +02:00
var f = new ExistingFile ( rd );
string current = f . TargetPath ;
yield return f ;
2014-11-04 15:34:20 +01:00
2025-05-13 08:33:09 +02:00
more = f . HasMore ;
while ( more && current == f . TargetPath )
more = await rd . ReadAsync ();
2025-03-19 22:23:57 +01:00
}
2025-05-13 08:33:09 +02:00
}
2025-05-16 14:46:08 +02:00
await db . Transaction . CommitAsync ();
2014-11-04 15:34:20 +01:00
}
}
2025-05-13 08:33:09 +02:00
public IAsyncEnumerable < IExistingFile > GetExistingFilesWithBlocks ()
2013-03-08 22:24:54 +01:00
{
2025-04-03 15:46:20 +02:00
if ( string . IsNullOrWhiteSpace ( m_tempfiletable ) || string . IsNullOrWhiteSpace ( m_tempblocktable ))
throw new InvalidOperationException ( "No temporary file table set up for this restore." );
2025-05-16 15:49:40 +02:00
return ExistingFile . GetExistingFilesWithBlocks ( this , m_tempfiletable );
2014-11-04 15:34:20 +01:00
}
private class LocalBlockSource : ILocalBlockSource
{
private class BlockDescriptor : IBlockDescriptor
{
private class BlockSource : IBlockSource
{
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
public BlockSource ( SqliteDataReader rd ) { m_reader = rd ; }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
public string Path { get { return m_reader . ConvertValueToString ( 6 ) ?? "" ; } }
2014-11-04 15:34:20 +01:00
public long Offset { get { return m_reader . ConvertValueToInt64 ( 7 ); } }
2014-11-05 21:43:08 +01:00
public bool IsMetadata { get { return false ; } }
2014-11-04 15:34:20 +01:00
}
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
public BlockDescriptor ( SqliteDataReader rd ) { m_reader = rd ; HasMore = true ; }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
private string TargetPath { get { return m_reader . ConvertValueToString ( 0 ) ?? "" ; } }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
public string Hash { get { return m_reader . ConvertValueToString ( 2 ) ?? "" ; } }
2014-11-04 15:34:20 +01:00
public long Offset { get { return m_reader . ConvertValueToInt64 ( 3 ); } }
public long Index { get { return m_reader . ConvertValueToInt64 ( 4 ); } }
public long Size { get { return m_reader . ConvertValueToInt64 ( 5 ); } }
2014-11-05 21:43:08 +01:00
public bool IsMetadata { get { return !( m_reader . ConvertValueToInt64 ( 9 ) == 0 ); } }
2014-11-04 15:34:20 +01:00
public bool HasMore { get ; private set ; }
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IBlockSource > BlockSources ()
2014-11-04 15:34:20 +01:00
{
2025-05-16 14:46:08 +02:00
var p = TargetPath ;
var h = Hash ;
var s = Size ;
2014-11-04 15:34:20 +01:00
2025-05-16 14:46:08 +02:00
while ( HasMore && p == TargetPath && h == Hash && s == Size )
{
yield return new BlockSource ( m_reader );
HasMore = await m_reader . ReadAsync ();
2014-11-04 15:34:20 +01:00
}
}
2025-05-16 14:46:08 +02:00
2014-11-04 15:34:20 +01:00
}
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
public LocalBlockSource ( SqliteDataReader rd ) { m_reader = rd ; HasMore = true ; }
2014-11-04 15:34:20 +01:00
2025-04-03 15:46:20 +02:00
public string TargetPath { get { return m_reader . ConvertValueToString ( 0 ) ?? "" ; } }
2014-11-04 15:34:20 +01:00
public long TargetFileID { get { return m_reader . ConvertValueToInt64 ( 1 ); } }
public bool HasMore { get ; private set ; }
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IBlockDescriptor > Blocks ()
2014-11-04 15:34:20 +01:00
{
2025-05-16 14:46:08 +02:00
var p = TargetPath ;
while ( HasMore && p == TargetPath )
2014-11-04 15:34:20 +01:00
{
2025-05-16 14:46:08 +02:00
var c = new BlockDescriptor ( m_reader );
var h = c . Hash ;
var s = c . Size ;
2014-11-04 15:34:20 +01:00
2025-05-16 14:46:08 +02:00
yield return c ;
2014-11-04 15:34:20 +01:00
2025-05-16 14:46:08 +02:00
HasMore = c . HasMore ;
while ( HasMore && c . Hash == h && c . Size == s && TargetPath == p )
HasMore = await m_reader . ReadAsync ();
2014-11-04 15:34:20 +01:00
}
}
2025-05-16 15:49:40 +02:00
public static async IAsyncEnumerable < ILocalBlockSource > GetFilesAndSourceBlocks ( LocalDatabase db , string filetablename , string blocktablename , long blocksize , bool skipMetadata )
2014-11-04 15:34:20 +01:00
{
2025-03-19 22:23:57 +01:00
// TODO: Skip metadata as required
2025-04-30 18:32:43 +02:00
// Have to order by target path and hash, to ensure BlockDescriptor and BlockSource match adjacent rows
2025-05-16 15:49:40 +02:00
using var cmd = db . Connection . CreateCommand ( $@"
SELECT DISTINCT
""A"".""TargetPath"",
""A"".""ID"",
""B"".""Hash"",
(""B"".""Index"" * {blocksize}),
""B"".""Index"",
""B"".""Size"",
""C"".""Path"",
(""D"".""Index"" * {blocksize}),
""E"".""Size"",
""B"".""Metadata""
FROM
""{filetablename}"" ""A"",
""{blocktablename}"" ""B"",
""File"" ""C"",
""BlocksetEntry"" ""D"",
""Block"" E
2025-05-19 10:46:50 +02:00
WHERE
""A"".""ID"" = ""B"".""FileID""
AND ""C"".""BlocksetID"" = ""D"".""BlocksetID""
AND ""D"".""BlockID"" = ""E"".""ID""
AND ""B"".""Hash"" = ""E"".""Hash""
AND ""B"".""Size"" = ""E"".""Size""
AND ""B"".""Restored"" = 0
2025-05-16 15:49:40 +02:00
ORDER BY
""A"".""TargetPath"",
""B"".""Index""
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( db . Transaction );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
var more = await rd . ReadAsync ();
2025-05-16 15:49:40 +02:00
2025-05-13 08:33:09 +02:00
while ( more )
2014-11-04 15:34:20 +01:00
{
2025-05-13 08:33:09 +02:00
var f = new LocalBlockSource ( rd );
string current = f . TargetPath ;
yield return f ;
2014-11-04 15:34:20 +01:00
2025-05-13 08:33:09 +02:00
more = f . HasMore ;
while ( more && current == f . TargetPath )
more = await rd . ReadAsync ();
2014-11-04 15:34:20 +01:00
}
2025-05-16 15:49:40 +02:00
await db . Transaction . CommitAsync ();
2014-11-04 15:34:20 +01:00
}
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public IAsyncEnumerable < ILocalBlockSource > GetFilesAndSourceBlocks ( bool skipMetadata , long blocksize )
2013-03-08 22:24:54 +01:00
{
2025-04-03 15:46:20 +02:00
if ( string . IsNullOrWhiteSpace ( m_tempfiletable ) || string . IsNullOrWhiteSpace ( m_tempblocktable ))
throw new InvalidOperationException ( "No temporary file table set up for this restore." );
2025-05-16 15:49:40 +02:00
return LocalBlockSource . GetFilesAndSourceBlocks ( this , m_tempfiletable , m_tempblocktable , blocksize , skipMetadata );
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < IRemoteVolume > GetMissingVolumes ()
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT
""RV"".""Name"",
""RV"".""Hash"",
""RV"".""Size"",
""BB"".""MaxIndex""
FROM ""RemoteVolume"" ""RV""
INNER JOIN (
SELECT
""TB"".""VolumeID"",
MAX(""TB"".""Index"") as ""MaxIndex""
FROM ""{m_tempblocktable}"" ""TB""
WHERE ""TB"".""Restored"" = 0
GROUP BY ""TB"".""VolumeID""
) as ""BB""
ON ""RV"".""ID"" = ""BB"".""VolumeID""
ORDER BY ""BB"".""MaxIndex""
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
2025-05-13 08:33:09 +02:00
// Return order from SQLite-DISTINCT is likely to be sorted by Name, which is bad for restore.
// If the end of very large files (e.g. iso's) is restored before the beginning, most OS write out zeros to fill the file.
// If we manage to get the volumes in an order restoring front blocks first, this can save time.
// An optimal algorithm would build a dependency net with cycle resolution to find the best near topological
// order of volumes, but this is a bit too fancy here.
// We will just put a very simple heuristic to work, that will try to prefer volumes containing lower block indexes:
// We just order all volumes by the maximum block index they contain. This query is slow, but should be worth the effort.
// Now it is likely to restore all files from front to back. Large files will always be done last.
// One could also use like the average block number in a volume, that needs to be measured.
using var rd = await cmd . ExecuteReaderAsync ();
object [] r = new object [ 3 ];
while ( await rd . ReadAsync ())
{
rd . GetValues ( r );
yield return new RemoteVolume (
rd . ConvertValueToString ( 0 ),
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToInt64 ( 2 , - 1 )
);
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
2014-11-05 21:43:08 +01:00
public interface IFilesAndMetadata : IDisposable
{
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IVolumePatch > FilesWithMissingBlocks ();
IAsyncEnumerable < IVolumePatch > MetadataWithMissingBlocks ();
2014-11-05 21:43:08 +01:00
}
private class FilesAndMetadata : IFilesAndMetadata
{
2025-05-16 15:48:27 +02:00
private string m_tmptable = null !;
private string m_filetablename = null !;
private string m_blocktablename = null !;
private long m_blocksize ;
2014-11-05 21:43:08 +01:00
2025-05-16 15:48:27 +02:00
private LocalDatabase m_db = null !;
2014-11-05 21:43:08 +01:00
2025-05-16 15:48:27 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateAsync instead.")]
public FilesAndMetadata ( SqliteConnection connection , string filetablename , string blocktablename , long blocksize , BlockVolumeReader curvolume )
2014-11-05 21:43:08 +01:00
{
2025-05-16 15:48:27 +02:00
throw new NotImplementedException ( "Use CreateAsync instead of the constructor" );
}
private FilesAndMetadata () { }
public static async Task < FilesAndMetadata > CreateAsync ( LocalDatabase db , string filetablename , string blocktablename , long blocksize , BlockVolumeReader curvolume )
{
var fam = new FilesAndMetadata ()
{
m_db = db ,
m_filetablename = filetablename ,
m_blocktablename = blocktablename ,
m_blocksize = blocksize ,
};
using var c = db . Connection . CreateCommand ()
2025-05-19 10:49:22 +02:00
. SetTransaction ( db . Transaction );
2025-05-16 15:48:27 +02:00
fam . m_tmptable = "VolumeFiles-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
await c . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{fam.m_tmptable}"" (
""Hash"" TEXT NOT NULL,
""Size"" INTEGER NOT NULL
)
" );
c . SetCommandAndParameters ( $@"
INSERT INTO ""{fam.m_tmptable}"" (
""Hash"",
""Size""
)
VALUES (
@Hash,
@Size)
" );
2025-05-13 08:33:09 +02:00
foreach ( var s in curvolume . Blocks )
2014-11-05 21:43:08 +01:00
{
2025-05-16 15:48:27 +02:00
await c . SetParameterValue ( "@Hash" , s . Key )
. SetParameterValue ( "@Size" , s . Value )
. ExecuteNonQueryAsync ();
2025-05-13 08:33:09 +02:00
}
2014-11-05 21:43:08 +01:00
2025-05-13 08:33:09 +02:00
// The index _HashSizeIndex is not needed anymore. Index on "Blocks-..." is used on Join in GetMissingBlocks
2015-11-16 12:57:34 +01:00
2025-05-16 15:48:27 +02:00
await db . Transaction . CommitAsync ();
return fam ;
2014-11-05 21:43:08 +01:00
}
public void Dispose ()
2025-05-16 15:49:40 +02:00
{
DisposeAsync (). Await ();
}
public async Task DisposeAsync ()
2014-11-05 21:43:08 +01:00
{
if ( m_tmptable != null )
2025-05-13 08:33:09 +02:00
{
2025-05-16 15:49:40 +02:00
using var c = m_db . Connection . CreateCommand ( @"DROP TABLE IF EXISTS ""{m_tmptable}""" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_db . Transaction );
2025-05-16 15:49:40 +02:00
await c . ExecuteNonQueryAsync ();
await m_db . Transaction . CommitAsync ();
2025-05-13 08:33:09 +02:00
}
2014-11-05 21:43:08 +01:00
}
private class VolumePatch : IVolumePatch
{
private class PatchBlock : IPatchBlock
{
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
public PatchBlock ( SqliteDataReader rd ) { m_reader = rd ; }
2014-11-05 21:43:08 +01:00
public long Offset { get { return m_reader . ConvertValueToInt64 ( 2 ); } }
public long Size { get { return m_reader . ConvertValueToInt64 ( 3 ); } }
2025-04-03 15:46:20 +02:00
public string Key { get { return m_reader . ConvertValueToString ( 4 ) ?? "" ; } }
2014-11-05 21:43:08 +01:00
}
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_reader ;
public VolumePatch ( SqliteDataReader rd ) { m_reader = rd ; HasMore = true ; }
2014-11-05 21:43:08 +01:00
2025-04-03 15:46:20 +02:00
public string Path { get { return m_reader . ConvertValueToString ( 0 ) ?? "" ; } }
2014-11-05 21:43:08 +01:00
public long FileID { get { return m_reader . ConvertValueToInt64 ( 1 ); } }
2024-11-08 05:56:48 +01:00
public bool HasMore { get ; private set ; }
2014-11-05 21:43:08 +01:00
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IPatchBlock > Blocks ()
2014-11-05 21:43:08 +01:00
{
2025-05-16 14:46:08 +02:00
string p = Path ;
while ( HasMore && p == Path )
2014-11-05 21:43:08 +01:00
{
2025-05-16 14:46:08 +02:00
yield return new PatchBlock ( m_reader );
HasMore = await m_reader . ReadAsync ();
2014-11-05 21:43:08 +01:00
}
}
}
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IVolumePatch > FilesWithMissingBlocks ()
2014-11-05 21:43:08 +01:00
{
2025-05-16 14:46:08 +02:00
// The IN-clause with subquery enables SQLite to use indexes better. Three way join (A,B,C) is slow here!
2025-05-16 15:49:40 +02:00
using var cmd = m_db . Connection . CreateCommand ( $@"
SELECT DISTINCT
""A"".""TargetPath"",
""BB"".""FileID"",
(""BB"".""Index"" * {m_blocksize}),
""BB"".""Size"",
""BB"".""Hash""
FROM
""{m_filetablename}"" ""A"",
""{m_blocktablename}"" ""BB""
2025-05-19 10:46:50 +02:00
WHERE
""A"".""ID"" = ""BB"".""FileID""
AND ""BB"".""Restored"" = 0
AND ""BB"".""Metadata"" = {" 0 "}
AND "" BB "" . "" ID "" IN (
SELECT "" B "" . "" ID ""
FROM
"" { m_blocktablename } "" "" B "" ,
"" { m_tmptable } "" "" C ""
WHERE
"" B "" . "" Hash "" = "" C "" . "" Hash ""
AND "" B "" . "" Size "" = "" C "" . "" Size ""
)
2025-05-16 15:49:40 +02:00
ORDER BY
"" A "" . "" TargetPath "" ,
"" BB "" . "" Index ""
")
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_db . Transaction );
2025-05-16 15:49:40 +02:00
2025-05-16 14:46:08 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
if ( await rd . ReadAsync ())
{
var more = true ;
while ( more )
2025-05-13 08:33:09 +02:00
{
2025-05-16 14:46:08 +02:00
var f = new VolumePatch ( rd );
var current = f . Path ;
yield return f ;
2025-05-13 08:33:09 +02:00
2025-05-16 14:46:08 +02:00
more = f . HasMore ;
while ( more && current == f . Path )
more = await rd . ReadAsync ();
2014-11-05 21:43:08 +01:00
}
}
2025-05-16 14:46:08 +02:00
await m_db . Transaction . Transaction . CommitAsync ();
2014-11-05 21:43:08 +01:00
}
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IVolumePatch > MetadataWithMissingBlocks ()
2014-11-05 21:43:08 +01:00
{
2025-05-16 14:46:08 +02:00
// The IN-clause with subquery enables SQLite to use indexes better. Three way join (A,B,C) is slow here!
2025-05-16 15:49:40 +02:00
using var cmd = m_db . Connection . CreateCommand ( $@"
SELECT DISTINCT
""A"".""TargetPath"",
""BB"".""FileID"",
(""BB"".""Index"" * {m_blocksize}),
""BB"".""Size"",
""BB"".""Hash""
FROM
""{m_filetablename}"" ""A"",
""{m_blocktablename}"" ""BB""
2025-05-19 10:46:50 +02:00
WHERE
""A"".""ID"" = ""BB"".""FileID""
AND ""BB"".""Restored"" = 0
AND ""BB"".""Metadata"" = {" 1 "}
AND "" BB "" . "" ID "" IN (
SELECT "" B "" . "" ID ""
FROM
"" { m_blocktablename } "" "" B "" ,
"" { m_tmptable } "" "" C ""
WHERE
"" B "" . "" Hash "" = "" C "" . "" Hash ""
AND "" B "" . "" Size "" = "" C "" . "" Size ""
)
2025-05-16 15:49:40 +02:00
ORDER BY
"" A "" . "" TargetPath "" ,
"" BB "" . "" Index ""
")
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_db . Transaction );
2025-05-16 15:49:40 +02:00
2025-05-16 14:46:08 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
if ( await rd . ReadAsync ())
{
var more = true ;
while ( more )
2025-05-13 08:33:09 +02:00
{
2025-05-16 14:46:08 +02:00
var f = new VolumePatch ( rd );
string current = f . Path ;
yield return f ;
2025-05-13 08:33:09 +02:00
2025-05-16 14:46:08 +02:00
more = f . HasMore ;
while ( more && current == f . Path )
more = await rd . ReadAsync ();
2014-11-05 21:43:08 +01:00
}
}
}
}
2025-05-16 15:49:40 +02:00
public async Task < IFilesAndMetadata > GetMissingBlockData ( BlockVolumeReader curvolume , long blocksize )
2013-03-08 22:24:54 +01:00
{
2025-04-03 15:46:20 +02:00
if ( string . IsNullOrWhiteSpace ( m_tempfiletable ) || string . IsNullOrWhiteSpace ( m_tempblocktable ))
throw new InvalidOperationException ( "No temporary file table set up for this restore." );
2025-05-16 15:49:40 +02:00
return await FilesAndMetadata . CreateAsync ( this , m_tempfiletable , m_tempblocktable , blocksize , curvolume );
2013-03-08 22:24:54 +01:00
}
2025-02-27 14:14:07 +01:00
/// <summary>
/// Returns a connection from the connection pool.
/// </summary>
/// <returns>A connection from the connection pool.</returns>
2025-05-23 15:46:30 +02:00
public async Task <( SqliteConnection , ReusableTransaction )> GetConnectionFromPool ()
2025-02-27 14:14:07 +01:00
{
2025-05-23 15:46:30 +02:00
if (! m_connection_pool . TryTake ( out var entry ))
2025-02-27 14:14:07 +01:00
{
2025-05-23 15:46:30 +02:00
var connection = await SQLiteLoader . LoadConnectionAsync ();
2025-05-16 15:49:40 +02:00
connection . ConnectionString = m_connection . ConnectionString + ";Cache=Shared" ;
2025-05-23 15:46:30 +02:00
await connection . OpenAsync ();
2025-05-16 15:49:40 +02:00
await SQLiteLoader . ApplyCustomPragmasAsync ( connection , m_pagecachesize );
2025-05-23 15:46:30 +02:00
var transaction = new ReusableTransaction ( connection );
return ( connection , transaction );
2025-02-27 14:14:07 +01:00
}
2025-05-23 15:46:30 +02:00
return entry ;
2025-02-27 14:14:07 +01:00
}
2016-09-15 11:39:27 +02:00
private class FileToRestore : IFileToRestore
{
public string Path { get ; private set ; }
public string Hash { get ; private set ; }
2015-03-04 16:23:44 +01:00
public long Length { get ; private set ; }
2024-11-08 05:56:48 +01:00
2015-03-04 16:23:44 +01:00
public FileToRestore ( long id , string path , string hash , long length )
2016-09-15 11:39:27 +02:00
{
2025-03-14 14:34:56 +01:00
Path = path ;
Hash = hash ;
Length = length ;
2016-09-15 11:39:27 +02:00
}
}
2013-05-20 13:46:58 +02:00
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < IFileToRestore > GetFilesToRestore ( bool onlyNonVerified )
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT
""{m_tempfiletable}"".""ID"",
""{m_tempfiletable}"".""TargetPath"",
""Blockset"".""FullHash"",
""Blockset"".""Length""
FROM
""{m_tempfiletable}"",
""Blockset""
2025-05-19 10:46:50 +02:00
WHERE
""{m_tempfiletable}"".""BlocksetID"" = ""Blockset"".""ID""
AND ""{m_tempfiletable}"".""DataVerified"" <= @Verified
2025-05-16 15:49:40 +02:00
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr )
2025-05-16 15:49:40 +02:00
. SetParameterValue ( "@Verified" , ! onlyNonVerified );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
yield return new FileToRestore (
rd . ConvertValueToInt64 ( 0 ), rd . ConvertValueToString ( 1 ) ?? "" , rd . ConvertValueToString ( 2 ) ?? "" , rd . ConvertValueToInt64 ( 3 ));
2025-03-19 21:42:08 +01:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
2024-11-29 03:41:41 +01:00
/// <summary>
/// Returns a list of files and symlinks to restore.
/// </summary>
/// <returns>A list of files and symlinks to restore.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < FileRequest > GetFilesAndSymlinksToRestore ()
2024-11-29 03:41:41 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT
F.ID,
F.Path,
F.TargetPath,
IFNULL(B.FullHash, ''),
IFNULL(B.Length, 0),
F.BlocksetID
2024-11-29 03:41:41 +01:00
FROM ""{m_tempfiletable}"" F
2025-05-16 15:49:40 +02:00
LEFT JOIN Blockset B
ON F.BlocksetID = B.ID
WHERE F.BlocksetID != {FOLDER_BLOCKSET_ID}
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
2024-12-03 08:35:14 +01:00
yield return new FileRequest ( rd . ConvertValueToInt64 ( 0 ), rd . ConvertValueToString ( 1 ), rd . ConvertValueToString ( 2 ), rd . ConvertValueToString ( 3 ), rd . ConvertValueToInt64 ( 4 ), rd . ConvertValueToInt64 ( 5 ));
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2024-12-03 08:35:14 +01:00
}
2025-03-24 20:24:37 +01:00
/// <summary>
/// Returns a list of folders to restore. Used to restore folder metadata.
/// </summary>
/// <returns>A list of folders to restore.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < FileRequest > GetFolderMetadataToRestore ()
2025-03-24 20:24:37 +01:00
{
using var cmd = m_connection . CreateCommand ();
2025-05-23 15:46:30 +02:00
cmd . SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
using var rd = await cmd . ExecuteReaderAsync ( $@"
SELECT
F.ID,
'',
F.TargetPath,
'',
0,
{FOLDER_BLOCKSET_ID}
2025-03-24 20:24:37 +01:00
FROM ""{m_tempfiletable}"" F
2025-05-19 10:46:50 +02:00
WHERE
F.BlocksetID = {FOLDER_BLOCKSET_ID}
AND F.MetadataID IS NOT NULL
AND F.MetadataID >= 0
2025-05-16 15:49:40 +02:00
" );
2025-03-24 20:24:37 +01:00
2025-05-13 08:33:09 +02:00
while ( await rd . ReadAsync ())
2025-05-16 15:49:40 +02:00
yield return new FileRequest (
rd . ConvertValueToInt64 ( 0 ),
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 2 ),
rd . ConvertValueToString ( 3 ),
rd . ConvertValueToInt64 ( 4 ),
rd . ConvertValueToInt64 ( 5 )
);
2025-03-24 20:24:37 +01:00
}
2024-12-03 08:35:14 +01:00
/// <summary>
/// Returns a list of blocks and their volume IDs. Used by the <see cref="BlockManager"/> to keep track of blocks and volumes to automatically evict them from the respective caches.
/// </summary>
/// <param name="skipMetadata">Flag indicating whether the returned blocks should exclude the metadata blocks.</param>
/// <returns>A list of tuples containing the block ID and the volume ID of the block.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable <( long , long )> GetBlocksAndVolumeIDs ( bool skipMetadata )
2024-12-03 08:35:14 +01:00
{
2025-05-16 15:49:40 +02:00
var metadata_query = skipMetadata ? "" : $@"
2024-12-03 08:35:14 +01:00
UNION ALL
2025-05-16 15:49:40 +02:00
SELECT
""Block"".""ID"",
""Block"".""VolumeID""
2024-12-03 08:35:14 +01:00
FROM ""{m_tempfiletable}""
2025-05-16 15:49:40 +02:00
INNER JOIN ""Metadataset""
ON ""{m_tempfiletable}"".""MetadataID"" = ""Metadataset"".""ID""
INNER JOIN ""BlocksetEntry""
ON ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
INNER JOIN ""Block""
ON ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
" ;
using var cmd = Connection . CreateCommand ( $@"
SELECT
""Block"".""ID"",
""Block"".""VolumeID""
FROM ""BlocksetEntry""
INNER JOIN ""{m_tempfiletable}""
ON ""BlocksetEntry"".""BlocksetID"" = ""{m_tempfiletable}"".""BlocksetID""
INNER JOIN ""Block""
ON ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
{metadata_query}
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
using var reader = await cmd . ExecuteReaderAsync ();
2025-05-13 08:33:09 +02:00
while ( await reader . ReadAsync ())
2025-05-16 15:49:40 +02:00
yield return (
reader . ConvertValueToInt64 ( 0 ),
reader . ConvertValueToInt64 ( 1 )
);
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2024-12-03 08:35:14 +01:00
}
/// <summary>
/// Returns a list of <see cref="BlockRequest"/> for the given blockset ID. It is used by the <see cref="FileProcessor"/> to restore the blocks of a file.
/// </summary>
/// <param name="blocksetID">The BlocksetID of the file.</param>
/// <returns>A list of <see cref="BlockRequest"/> needed to restore the given file.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < BlockRequest > GetBlocksFromFile ( long blocksetID )
2024-12-03 08:35:14 +01:00
{
2025-05-23 15:46:30 +02:00
var ( connection , transaction ) = await GetConnectionFromPool ();
try
{
using var cmd = connection . CreateCommand ( @ $"
2025-05-16 15:49:40 +02:00
SELECT
""Block"".""ID"",
""Block"".""Hash"",
""Block"".""Size"",
""Block"".""VolumeID""
FROM ""BlocksetEntry""
INNER JOIN ""Block""
ON ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
WHERE ""BlocksetEntry"".""BlocksetID"" = @BlocksetID
" )
2025-05-23 15:46:30 +02:00
. SetTransaction ( transaction )
. SetParameterValue ( "@BlocksetID" , blocksetID );
using var reader = await cmd . ExecuteReaderAsync ();
for ( long i = 0 ; await reader . ReadAsync (); i ++)
yield return new BlockRequest (
reader . ConvertValueToInt64 ( 0 ),
i ,
reader . ConvertValueToString ( 1 ),
reader . ConvertValueToInt64 ( 2 ),
reader . ConvertValueToInt64 ( 3 ),
false
);
}
finally
{
// Return the connection to the pool
m_connection_pool . Add (( connection , transaction ));
}
2024-12-03 08:35:14 +01:00
}
/// <summary>
/// Returns a list of <see cref="BlockRequest"/> for the metadata blocks of the given file. It is used by the <see cref="FileProcessor"/> to restore the metadata of a file.
/// </summary>
/// <param name="fileID">The ID of the file.</param>
/// <returns>A list of <see cref="BlockRequest"/> needed to restore the metadata of the given file.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < BlockRequest > GetMetadataBlocksFromFile ( long fileID )
2024-12-03 08:35:14 +01:00
{
2025-05-23 15:46:30 +02:00
var ( connection , transaction ) = await GetConnectionFromPool ();
try
{
using var cmd = connection . CreateCommand ( $@"
2025-05-16 15:49:40 +02:00
SELECT
""Block"".""ID"",
""Block"".""Hash"",
""Block"".""Size"",
""Block"".""VolumeID""
2025-03-19 22:23:57 +01:00
FROM ""File""
2025-05-16 15:49:40 +02:00
INNER JOIN ""Metadataset""
ON ""File"".""MetadataID"" = ""Metadataset"".""ID""
INNER JOIN ""BlocksetEntry""
ON ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
INNER JOIN ""Block""
ON ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
2025-03-19 22:23:57 +01:00
WHERE ""File"".""ID"" = @FileID
2025-05-16 15:49:40 +02:00
" )
2025-05-23 15:46:30 +02:00
. SetTransaction ( transaction )
. SetParameterValue ( "@FileID" , fileID );
2025-02-27 14:14:07 +01:00
2025-05-23 15:46:30 +02:00
using var reader = await cmd . ExecuteReaderAsync ();
for ( long i = 0 ; await reader . ReadAsync (); i ++)
{
yield return new BlockRequest ( reader . ConvertValueToInt64 ( 0 ), i , reader . ConvertValueToString ( 1 ), reader . ConvertValueToInt64 ( 2 ), reader . ConvertValueToInt64 ( 3 ), false );
}
}
finally
2024-12-03 08:35:14 +01:00
{
2025-05-23 15:46:30 +02:00
// Return the connection to the pool
m_connection_pool . Add (( connection , transaction ));
2024-12-03 08:35:14 +01:00
}
}
/// <summary>
/// Returns the volume information for the given volume ID. It is used by the <see cref="VolumeManager"/> to get the volume information for the given volume ID.
/// </summary>
/// <param name="VolumeID">The ID of the volume.</param>
/// <returns>A tuple containing the name, size, and hash of the volume.</returns>
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable <( string , long , string )> GetVolumeInfo ( long VolumeID )
2024-12-03 08:35:14 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( @"
SELECT
Name,
Size,
Hash
FROM RemoteVolume
WHERE ID = @VolumeID
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr )
2025-05-16 15:49:40 +02:00
. SetParameterValue ( "@VolumeID" , VolumeID );
2025-05-13 08:33:09 +02:00
using var reader = await cmd . ExecuteReaderAsync ();
while ( await reader . ReadAsync ())
2025-04-03 15:46:20 +02:00
yield return ( reader . ConvertValueToString ( 0 ) ?? "" , reader . ConvertValueToInt64 ( 1 ), reader . ConvertValueToString ( 2 ) ?? "" );
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2024-11-29 03:41:41 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task DropRestoreTable ()
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ()
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
2025-05-13 08:33:09 +02:00
if ( m_tempfiletable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tempfiletable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_tempfiletable = null ; }
2013-03-08 22:24:54 +01:00
2025-05-13 08:33:09 +02:00
if ( m_tempblocktable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tempblocktable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_tempblocktable = null ; }
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
if ( m_latestblocktable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_latestblocktable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_latestblocktable = null ; }
2022-01-31 14:26:21 +01:00
2025-05-13 08:33:09 +02:00
if ( m_fileprogtable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_fileprogtable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_fileprogtable = null ; }
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
if ( m_totalprogtable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_totalprogtable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_totalprogtable = null ; }
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
if ( m_filesnewlydonetable != null )
try
{
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_filesnewlydonetable}""" );
2025-05-13 08:33:09 +02:00
}
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "CleanupError" , ex , "Cleanup error: {0}" , ex . Message ); }
finally { m_filesnewlydonetable = null ; }
2024-11-08 05:56:48 +01:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
public interface IBlockMarker : IDisposable
{
2025-05-13 08:33:09 +02:00
Task SetBlockRestored ( long targetfileid , long index , string hash , long blocksize , bool metadata );
Task SetAllBlocksMissing ( long targetfileid );
Task SetAllBlocksRestored ( long targetfileid , bool includeMetadata );
Task SetFileDataVerified ( long targetfileid );
Task CommitAsync ();
Task UpdateProcessed ( IOperationProgressUpdater writer );
2013-03-08 22:24:54 +01:00
}
2016-02-27 22:04:48 +01:00
/// <summary>
/// A new implementation of IBlockMarker, marking the blocks directly in the blocks table as restored
/// and reading statistics about progress from DB (kept up-to-date by triggers).
/// There is no negative influence on performance, esp. since the block table is temporary anyway.
/// </summary>
private class DirectBlockMarker : IBlockMarker
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:48:27 +02:00
private SqliteCommand m_insertblockCommand = null !;
private SqliteCommand m_resetfileCommand = null !;
private SqliteCommand m_updateAsRestoredCommand = null !;
private SqliteCommand m_updateFileAsDataVerifiedCommand = null !;
private SqliteCommand m_statUpdateCommand = null !;
private ReusableTransaction m_rtr = null !;
2013-08-23 22:18:13 +02:00
private bool m_hasUpdates = false ;
2016-02-27 22:04:48 +01:00
2025-05-16 15:48:27 +02:00
private string m_blocktablename = null !;
private string m_filetablename = null !;
2016-02-27 22:04:48 +01:00
2025-05-16 15:48:27 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateAsync instead.")]
2025-05-13 08:33:09 +02:00
public DirectBlockMarker ( SqliteConnection connection , string blocktablename , string filetablename , string statstablename )
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:48:27 +02:00
throw new NotImplementedException ( "Use CreateAsync instead of the constructor" );
}
2025-05-13 08:33:09 +02:00
2025-05-16 15:48:27 +02:00
private DirectBlockMarker () { }
2016-03-05 02:00:28 +01:00
2025-05-16 15:48:27 +02:00
public static async Task < DirectBlockMarker > CreateAsync ( LocalDatabase db , string blocktablename , string filetablename , string statstablename )
{
var dbm = new DirectBlockMarker ()
2016-02-27 22:04:48 +01:00
{
2025-05-16 15:48:27 +02:00
m_rtr = db . Transaction ,
m_blocktablename = blocktablename ,
m_filetablename = filetablename ,
m_insertblockCommand = await db . Connection . CreateCommandAsync ( $@"
UPDATE ""{blocktablename}"" SET ""Restored"" = 1
2025-05-19 10:46:50 +02:00
WHERE
""FileID"" = @TargetFileId
AND ""Index"" = @Index
AND ""Hash"" = @Hash
AND ""Size"" = @Size
AND ""Metadata"" = @Metadata
AND ""Restored"" = 0
2025-05-16 15:48:27 +02:00
" ),
m_resetfileCommand = await db . Connection . CreateCommandAsync ( $@"
UPDATE ""{blocktablename}""
SET ""Restored"" = 0
WHERE ""FileID"" = @TargetFileId
" ),
m_updateAsRestoredCommand = await db . Connection . CreateCommandAsync ( $@"
UPDATE ""{blocktablename}""
SET ""Restored"" = 1
2025-05-19 10:46:50 +02:00
WHERE
""FileID"" = @TargetFileId
AND ""Metadata"" <= @Metadata
2025-05-16 15:48:27 +02:00
" ),
m_updateFileAsDataVerifiedCommand = await db . Connection . CreateCommandAsync ( $@"
UPDATE ""{filetablename}""
SET ""DataVerified"" = 1
WHERE ""ID"" = @TargetFileId
" ),
m_statUpdateCommand = statstablename == null ?
// very slow fallback if stats tables were not created
2025-05-19 10:46:50 +02:00
await db . Connection . CreateCommandAsync ( $@"
SELECT
COUNT(DISTINCT ""FileID""),
SUM(""Size"")
FROM ""{blocktablename}""
WHERE ""Restored"" = 1
" )
2025-05-16 15:48:27 +02:00
:
// Fields in Stats: TotalFiles, TotalBlocks, TotalSize
// FilesFullyRestored, FilesPartiallyRestored, BlocksRestored, SizeRestored
2025-05-19 10:46:50 +02:00
await db . Connection . CreateCommandAsync ( $@"
SELECT
SUM(""FilesFullyRestored""),
SUM(""SizeRestored"")
FROM ""{statstablename}""
" )
2025-05-16 15:48:27 +02:00
};
2025-05-19 10:49:22 +02:00
dbm . m_insertblockCommand . SetTransaction ( db . Transaction );
dbm . m_resetfileCommand . SetTransaction ( db . Transaction );
dbm . m_updateAsRestoredCommand . SetTransaction ( db . Transaction );
dbm . m_updateFileAsDataVerifiedCommand . SetTransaction ( db . Transaction );
dbm . m_statUpdateCommand . SetTransaction ( db . Transaction );
2025-05-16 15:48:27 +02:00
return dbm ;
2013-08-23 22:18:13 +02:00
}
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
public async Task UpdateProcessed ( IOperationProgressUpdater updater )
2013-08-23 22:18:13 +02:00
{
if (! m_hasUpdates )
return ;
2016-02-27 22:04:48 +01:00
2013-08-23 22:18:13 +02:00
m_hasUpdates = false ;
2025-05-13 08:33:09 +02:00
using var rd = await m_statUpdateCommand . ExecuteReaderAsync ();
var filesprocessed = 0L ;
var processedsize = 0L ;
2013-08-23 22:18:13 +02:00
2025-05-13 08:33:09 +02:00
if ( rd . Read ())
{
filesprocessed += rd . ConvertValueToInt64 ( 0 , 0 );
processedsize += rd . ConvertValueToInt64 ( 1 , 0 );
2013-08-23 22:18:13 +02:00
}
2025-05-13 08:33:09 +02:00
updater . UpdatefilesProcessed ( filesprocessed , processedsize );
2013-08-22 20:52:54 +02:00
}
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
public async Task SetAllBlocksMissing ( long targetfileid )
2013-08-22 20:52:54 +02:00
{
2013-08-23 22:18:13 +02:00
m_hasUpdates = true ;
2025-05-13 08:33:09 +02:00
m_resetfileCommand . SetParameterValue ( "@TargetFileId" , targetfileid );
2025-05-16 15:49:40 +02:00
2025-05-13 08:33:09 +02:00
var r = await m_resetfileCommand . ExecuteNonQueryAsync ();
2013-08-22 20:52:54 +02:00
if ( r <= 0 )
throw new Exception ( "Unexpected reset result" );
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task SetAllBlocksRestored ( long targetfileid , bool includeMetadata )
2013-08-22 20:52:54 +02:00
{
2013-08-23 22:18:13 +02:00
m_hasUpdates = true ;
2025-05-16 15:49:40 +02:00
m_updateAsRestoredCommand . SetParameterValue ( "@TargetFileId" , targetfileid )
. SetParameterValue ( "@Metadata" , includeMetadata ? 1 : 0 );
2025-05-13 08:33:09 +02:00
var r = await m_updateAsRestoredCommand . ExecuteNonQueryAsync ();
2013-08-22 20:52:54 +02:00
if ( r <= 0 )
throw new Exception ( "Unexpected reset result" );
}
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
public async Task SetFileDataVerified ( long targetfileid )
2016-03-05 02:00:28 +01:00
{
m_hasUpdates = true ;
2025-05-13 08:33:09 +02:00
m_updateFileAsDataVerifiedCommand . SetParameterValue ( "@TargetFileId" , targetfileid );
2025-05-16 15:49:40 +02:00
2025-05-13 08:33:09 +02:00
var r = await m_updateFileAsDataVerifiedCommand . ExecuteNonQueryAsync ();
2016-03-12 12:48:41 +01:00
if ( r != 1 )
throw new Exception ( "Unexpected result when marking file as verified." );
2016-03-05 02:00:28 +01:00
}
2025-05-13 08:33:09 +02:00
public async Task SetBlockRestored ( long targetfileid , long index , string hash , long size , bool metadata )
2013-03-08 22:24:54 +01:00
{
2013-08-23 22:18:13 +02:00
m_hasUpdates = true ;
2025-05-16 15:49:40 +02:00
m_insertblockCommand . SetParameterValue ( "@TargetFileId" , targetfileid )
. SetParameterValue ( "@Index" , index )
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
. SetParameterValue ( "@Metadata" , metadata );
2025-05-13 08:33:09 +02:00
var r = await m_insertblockCommand . ExecuteNonQueryAsync ();
2013-03-08 22:24:54 +01:00
if ( r != 1 )
2016-02-27 22:04:48 +01:00
throw new Exception ( "Unexpected result when marking block." );
2013-03-08 22:24:54 +01:00
}
2016-02-27 22:04:48 +01:00
2025-05-13 08:33:09 +02:00
public async Task CommitAsync ()
2013-03-08 22:24:54 +01:00
{
2018-03-12 14:07:11 +01:00
using ( new Logging . Timer ( LOGTAG , "CommitBlockMarker" , "CommitBlockMarker" ))
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2013-03-08 22:24:54 +01:00
}
public void Dispose ()
{
2025-03-14 14:34:56 +01:00
m_insertblockCommand ?. Dispose ();
m_resetfileCommand ?. Dispose ();
m_updateAsRestoredCommand ?. Dispose ();
m_updateFileAsDataVerifiedCommand ?. Dispose ();
m_statUpdateCommand ?. Dispose ();
2013-03-08 22:24:54 +01:00
}
}
2025-05-16 15:49:40 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateBlockMarkerAsync instead.")]
2013-03-08 22:24:54 +01:00
public IBlockMarker CreateBlockMarker ()
2025-05-16 15:49:40 +02:00
{
throw new NotImplementedException ( "Use CreateBlockMarkerAsync instead of the constructor" );
}
2025-05-16 15:48:27 +02:00
public async Task < IBlockMarker > CreateBlockMarkerAsync ()
2013-03-08 22:24:54 +01:00
{
2025-04-03 15:46:20 +02:00
if ( string . IsNullOrWhiteSpace ( m_tempfiletable ) || string . IsNullOrWhiteSpace ( m_tempblocktable ))
throw new InvalidOperationException ( "No temporary file table set up for this restore." );
if ( string . IsNullOrWhiteSpace ( m_totalprogtable ))
throw new InvalidOperationException ( "No progress table set up for this restore." );
2025-05-16 15:48:27 +02:00
return await DirectBlockMarker . CreateAsync ( this , m_tempblocktable , m_tempfiletable , m_totalprogtable );
2013-03-08 22:24:54 +01:00
}
public override void Dispose ()
{
2025-05-23 15:46:30 +02:00
DisposeAsync (). Await ();
}
public override async Task DisposeAsync ()
{
await DisposePoolAsync ();
await DropRestoreTable ();
await base . DisposeAsync ();
}
public async Task DisposePoolAsync ()
{
foreach ( var ( connection , transaction ) in m_connection_pool )
2025-02-26 16:08:55 +01:00
{
2025-05-23 15:46:30 +02:00
await transaction . DisposeAsync ();
await connection . CloseAsync ();
await connection . DisposeAsync ();
2025-02-26 16:08:55 +01:00
}
2025-02-27 14:14:07 +01:00
m_connection_pool . Clear ();
2013-03-08 22:24:54 +01:00
}
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < string > GetTargetFolders ()
2013-03-08 22:24:54 +01:00
{
2025-05-16 15:49:40 +02:00
using var cmd = m_connection . CreateCommand ( $@"
SELECT ""TargetPath""
FROM ""{m_tempfiletable}""
WHERE ""BlocksetID"" == @BlocksetID
" )
2025-05-19 10:49:22 +02:00
. SetTransaction ( m_rtr )
2025-05-16 15:49:40 +02:00
. SetParameterValue ( "@BlocksetID" , FOLDER_BLOCKSET_ID );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
yield return rd . ConvertValueToString ( 0 ) ?? "" ;
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2016-09-15 11:39:27 +02:00
}
public interface IFastSource
{
string TargetPath { get ; }
long TargetFileID { get ; }
string SourcePath { get ; }
2025-05-16 14:46:08 +02:00
IAsyncEnumerable < IBlockEntry > Blocks ();
2016-09-15 11:39:27 +02:00
}
2024-11-08 05:56:48 +01:00
2016-09-15 11:39:27 +02:00
public interface IBlockEntry
{
long Offset { get ; }
long Size { get ; }
long Index { get ; }
string Hash { get ; }
2013-03-08 22:24:54 +01:00
}
2013-04-28 12:19:21 +02:00
2016-09-15 11:39:27 +02:00
private class FastSource : IFastSource
{
private class BlockEntry : IBlockEntry
{
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_rd ;
2018-05-23 21:18:01 -07:00
private readonly long m_blocksize ;
2025-05-13 08:33:09 +02:00
public BlockEntry ( SqliteDataReader rd , long blocksize ) { m_rd = rd ; m_blocksize = blocksize ; }
2025-01-28 08:54:50 +01:00
public long Offset { get { return m_rd . ConvertValueToInt64 ( 3 ) * m_blocksize ; } }
public long Index { get { return m_rd . ConvertValueToInt64 ( 3 ); } }
public long Size { get { return m_rd . ConvertValueToInt64 ( 5 ); } }
2025-04-03 15:46:20 +02:00
public string Hash { get { return m_rd . ConvertValueToString ( 4 ) ?? "" ; } }
2016-09-15 11:39:27 +02:00
}
2018-05-23 21:18:01 -07:00
2025-05-13 08:33:09 +02:00
private readonly SqliteDataReader m_rd ;
2018-05-23 21:18:01 -07:00
private readonly long m_blocksize ;
2025-05-13 08:33:09 +02:00
public FastSource ( SqliteDataReader rd , long blocksize ) { m_rd = rd ; m_blocksize = blocksize ; MoreData = true ; }
2016-09-15 11:39:27 +02:00
public bool MoreData { get ; private set ; }
2025-04-03 15:46:20 +02:00
public string TargetPath { get { return m_rd . ConvertValueToString ( 0 ) ?? "" ; } }
2025-01-28 08:54:50 +01:00
public long TargetFileID { get { return m_rd . ConvertValueToInt64 ( 2 ); } }
2025-04-03 15:46:20 +02:00
public string SourcePath { get { return m_rd . ConvertValueToString ( 1 ) ?? "" ; } }
2024-11-08 05:56:48 +01:00
2025-05-16 14:46:08 +02:00
public async IAsyncEnumerable < IBlockEntry > Blocks ()
2016-09-15 11:39:27 +02:00
{
2025-05-16 14:46:08 +02:00
var tid = TargetFileID ;
2024-11-08 05:56:48 +01:00
2025-05-16 14:46:08 +02:00
do
{
yield return new BlockEntry ( m_rd , m_blocksize );
} while (( MoreData = await m_rd . ReadAsync ()) && tid == TargetFileID );
2016-09-15 11:39:27 +02:00
}
}
2013-04-28 12:19:21 +02:00
2025-05-13 08:33:09 +02:00
public async IAsyncEnumerable < IFastSource > GetFilesAndSourceBlocksFast ( long blocksize )
2016-09-15 11:39:27 +02:00
{
2025-03-14 14:34:56 +01:00
using ( var cmdReader = m_connection . CreateCommand ())
2025-05-13 08:33:09 +02:00
using ( var cmd = m_connection . CreateCommand ())
2022-01-31 14:22:50 +01:00
{
2025-05-19 10:49:22 +02:00
cmdReader . SetTransaction ( m_rtr );
cmd . SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
cmd . SetCommandAndParameters ( $@"
UPDATE ""{m_tempfiletable}""
SET ""LocalSourceExists"" = 1
WHERE Path = @Path
" );
cmdReader . SetCommandAndParameters ( $@"
SELECT DISTINCT ""{m_tempfiletable}"".""Path""
FROM ""{m_tempfiletable}""
" );
using ( var rd = await cmdReader . ExecuteReaderAsync ())
2022-01-31 14:22:50 +01:00
{
2025-05-13 08:33:09 +02:00
while ( await rd . ReadAsync ())
2022-01-31 14:22:50 +01:00
{
2025-04-03 15:46:20 +02:00
var sourcepath = rd . ConvertValueToString ( 0 );
2025-03-14 14:34:56 +01:00
if ( SystemIO . IO_OS . FileExists ( sourcepath ))
2022-01-31 14:22:50 +01:00
{
2025-05-16 15:49:40 +02:00
await cmd . SetParameterValue ( "@Path" , sourcepath )
. ExecuteNonQueryAsync ();
2025-03-14 14:34:56 +01:00
}
else
{
Logging . Log . WriteVerboseMessage ( LOGTAG , "LocalSourceMissing" , "Local source file not found: {0}" , sourcepath );
2022-01-31 14:22:50 +01:00
}
}
}
2025-03-14 14:34:56 +01:00
//This localSourceExists index will make the query engine to start by searching FileSet table. As the result is ordered by FileSet.ID, we will get the cursor "instantly"
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_tempfiletable}_LocalSourceExists""
ON ""{m_tempfiletable}"" (""LocalSourceExists"")
" );
await m_rtr . CommitAsync ();
2022-01-31 14:22:50 +01:00
}
2022-01-31 14:26:21 +01:00
m_latestblocktable = "LatestBlocksetIds-" + m_temptabsetguid ;
2022-01-31 14:22:50 +01:00
2025-05-16 15:49:40 +02:00
var whereclause = $@"
""{m_tempfiletable}"".""LocalSourceExists"" = 1
AND ""{m_tempblocktable}"".""Restored"" = 0
AND ""{m_tempblocktable}"".""Metadata"" = 0
AND ""{m_tempfiletable}"".""TargetPath"" != ""{m_tempfiletable}"".""Path""
" ;
2025-01-03 11:06:49 +01:00
2025-05-16 15:49:40 +02:00
var latestBlocksetIds = $@"
2025-01-03 11:06:49 +01:00
SELECT
""File"".""Path"" AS ""PATH"",
""File"".""BlocksetID"" AS ""BlocksetID"",
MAX(""Fileset"".""Timestamp"") AS ""Timestamp""
FROM
""File"",
""FilesetEntry"",
""Fileset""
2025-05-19 10:46:50 +02:00
WHERE
""File"".""ID"" = ""FilesetEntry"".""FileID""
AND ""FilesetEntry"".""FilesetID"" = ""Fileset"".""ID""
AND ""File"".""Path"" IN (
SELECT DISTINCT ""{m_tempfiletable}"".""Path""
FROM
""{m_tempfiletable}"",
""{m_tempblocktable}""
WHERE
""{m_tempfiletable}"".""ID"" = ""{m_tempblocktable}"".""FileID""
AND {whereclause}
)
2025-05-16 15:49:40 +02:00
GROUP BY ""File"".""Path""
" ;
2022-01-31 14:22:50 +01:00
2020-08-26 10:10:54 +01:00
using ( var cmd = m_connection . CreateCommand ())
{
2025-05-19 10:49:22 +02:00
cmd . SetTransaction ( m_rtr );
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_latestblocktable}"" " );
await cmd . ExecuteNonQueryAsync ( $@"CREATE TEMPORARY TABLE ""{m_latestblocktable}"" AS {latestBlocksetIds}" );
await cmd . ExecuteNonQueryAsync ( $@"
CREATE INDEX ""{m_latestblocktable}_path""
ON ""{m_latestblocktable}"" (""Path"")
" );
2022-01-31 14:22:50 +01:00
2025-05-16 15:49:40 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
UPDATE ""{m_tempfiletable}""
SET LatestBlocksetId = (
SELECT BlocksetId
FROM ""{m_latestblocktable}""
WHERE Path = ""{m_tempfiletable}"".Path
)
" );
2020-08-26 10:10:54 +01:00
}
2025-05-16 15:49:40 +02:00
var sources = $@"
SELECT DISTINCT
""{m_tempfiletable}"".""TargetPath"",
""{m_tempfiletable}"".""Path"",
""{m_tempfiletable}"".""ID"",
""{m_tempblocktable}"".""Index"",
""{m_tempblocktable}"".""Hash"",
""{m_tempblocktable}"".""Size""
FROM
""{m_tempfiletable}"",
""{m_tempblocktable}"",
""BlocksetEntry""
2025-05-19 10:46:50 +02:00
WHERE
""{m_tempfiletable}"".""ID"" = ""{m_tempblocktable}"".""FileID""
AND ""BlocksetEntry"".""BlocksetID"" = ""{m_tempfiletable}"".""LatestBlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""{m_tempblocktable}"".""BlockID""
AND ""BlocksetEntry"".""Index"" = ""{m_tempblocktable}"".""Index""
AND {whereclause}
2025-05-16 15:49:40 +02:00
ORDER BY
""{m_tempfiletable}"".""ID"",
""{m_tempblocktable}"".""Index""
" ;
2022-01-31 14:22:50 +01:00
2020-08-26 10:10:54 +01:00
using ( var cmd = m_connection . CreateCommand ())
2016-09-15 11:39:27 +02:00
{
2025-05-19 10:49:22 +02:00
cmd . SetTransaction ( m_rtr );
2025-05-13 08:33:09 +02:00
using var rd = await cmd . ExecuteReaderAsync ( sources );
if ( await rd . ReadAsync ())
2016-09-15 11:39:27 +02:00
{
2025-03-14 14:34:56 +01:00
bool more ;
2016-09-15 11:39:27 +02:00
do
{
var n = new FastSource ( rd , blocksize );
var tid = n . TargetFileID ;
yield return n ;
2024-11-08 05:56:48 +01:00
2016-09-15 11:39:27 +02:00
more = n . MoreData ;
2025-01-28 08:54:50 +01:00
while ( more && n . TargetFileID == tid )
2025-05-13 08:33:09 +02:00
more = await rd . ReadAsync ();
2016-09-15 11:39:27 +02:00
} while ( more );
}
2022-01-31 14:22:50 +01:00
}
2025-05-13 08:33:09 +02:00
2025-05-16 15:49:40 +02:00
await m_rtr . CommitAsync ();
2016-09-15 11:39:27 +02:00
}
2013-04-28 12:19:21 +02:00
2013-03-08 22:24:54 +01:00
}
}