2025-01-14 19:42:03 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-12 16:56:41 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-01-14 19:42:03 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-12 16:56:41 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-14 19:42:03 +01:00
// all copies or substantial portions of the Software.
2025-05-12 16:56:41 +02:00
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-04-15 08:24:01 +02:00
// DEALINGS IN THE SOFTWARE.
2025-04-03 14:20:00 +02:00
#nullable enable
2024-02-28 15:45:30 +01:00
using System ;
2013-03-27 16:06:45 +01:00
using System.Collections.Generic ;
2019-04-22 21:11:27 -07:00
using System.Data ;
2025-05-12 16:58:23 +02:00
using System.IO ;
2013-03-27 16:06:45 +01:00
using System.Linq ;
2025-05-12 16:58:23 +02:00
using System.Runtime.CompilerServices ;
2013-03-27 16:06:45 +01:00
using System.Text ;
2025-05-12 16:58:23 +02:00
using System.Threading.Tasks ;
2018-11-14 08:47:01 -02:00
using Duplicati.Library.Modules.Builtin.ResultSerialization ;
2019-09-29 20:16:28 -07:00
using Duplicati.Library.Utility ;
2025-03-10 14:41:36 +01:00
using Duplicati.Library.Interface ;
2025-05-12 16:58:23 +02:00
using Microsoft.Data.Sqlite ;
2024-05-11 23:37:21 +02:00
// Expose internal classes to UnitTests, so that Database classes can be tested
[assembly: InternalsVisibleTo("Duplicati.UnitTest")]
2013-03-27 16:06:45 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Database
2013-03-27 16:06:45 +01:00
{
2013-08-22 20:52:54 +02:00
internal class LocalDatabase : IDisposable
2019-08-05 20:14:05 -04: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 ( LocalDatabase ));
2025-04-02 23:20:58 +02:00
/// <summary>
/// The chunk size for batch operations
/// </summary>
/// <remarks>SQLite has a limit of 999 parameters in a single statement</remarks>
public const int CHUNK_SIZE = 128 ;
2025-05-12 16:58:23 +02:00
protected readonly SqliteConnection m_connection ;
2013-03-27 16:06:45 +01:00
protected readonly long m_operationid = - 1 ;
2025-04-22 14:26:46 +02:00
protected readonly long m_pagecachesize ;
2025-01-14 19:42:03 +01:00
private bool m_hasExecutedVacuum ;
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
private readonly SqliteCommand m_updateremotevolumeCommand ;
private readonly SqliteCommand m_selectremotevolumesCommand ;
private readonly SqliteCommand m_selectremotevolumeCommand ;
private readonly SqliteCommand m_removeremotevolumeCommand ;
private readonly SqliteCommand m_removedeletedremotevolumeCommand ;
private readonly SqliteCommand m_selectremotevolumeIdCommand ;
private readonly SqliteCommand m_createremotevolumeCommand ;
private readonly SqliteCommand m_selectduplicateRemoteVolumesCommand ;
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
private readonly SqliteCommand m_insertlogCommand ;
private readonly SqliteCommand m_insertremotelogCommand ;
private readonly SqliteCommand m_insertIndexBlockLink ;
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
private readonly SqliteCommand m_findpathprefixCommand ;
private readonly SqliteCommand m_insertpathprefixCommand ;
2018-06-14 10:12:24 +02:00
2013-03-27 16:06:45 +01:00
public const long FOLDER_BLOCKSET_ID = - 100 ;
public const long SYMLINK_BLOCKSET_ID = - 200 ;
2013-03-08 22:24:54 +01:00
public DateTime OperationTimestamp { get ; private set ; }
2025-05-12 16:58:23 +02:00
internal SqliteConnection Connection { get { return m_connection ; } }
2019-08-05 20:14:05 -04:00
2013-05-25 16:40:15 +02:00
public bool IsDisposed { get ; private set ; }
2013-03-08 22:24:54 +01:00
2016-04-06 20:40:34 +02:00
public bool ShouldCloseConnection { get ; set ; }
2025-05-12 16:58:23 +02:00
protected static async Task < SqliteConnection > CreateConnection ( string path , long pagecachesize )
2013-03-08 22:24:54 +01:00
{
2025-03-14 14:34:56 +01:00
path = Path . GetFullPath ( path );
if (! Directory . Exists ( Path . GetDirectoryName ( path )))
2025-04-03 14:20:00 +02:00
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ?? throw new DirectoryNotFoundException ( "Path was a root folder." ));
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
var c = await SQLiteHelper . SQLiteLoader . LoadConnection ( path , pagecachesize );
2013-03-08 22:24:54 +01:00
2020-12-28 12:18:58 -08:00
try
{
2025-04-25 12:30:03 +02:00
SQLiteHelper . DatabaseUpgrader . UpgradeDatabase ( c , path , typeof ( DatabaseSchemaMarker ));
2020-12-28 12:18:58 -08:00
}
catch
{
//Don't leak database connections when something goes wrong
2025-05-12 16:58:23 +02:00
await c . DisposeAsync ();
2020-12-28 12:18:58 -08:00
throw ;
}
2019-08-05 20:14:05 -04:00
2013-03-27 16:06:45 +01:00
return c ;
}
2025-03-13 23:06:32 +01:00
/// <summary>
/// Formats the string using the invariant culture
/// </summary>
/// <param name="formattable">The formattable string</param>
/// <returns>The formatted string</returns>
public static string FormatInvariant ( FormattableString formattable )
=> Library . Utility . Utility . FormatInvariant ( formattable );
2024-02-09 17:10:05 +01:00
public static bool Exists ( string path )
{
return File . Exists ( path );
}
2013-03-27 16:06:45 +01:00
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
/// <param name="path">The path to the database</param>
2023-09-21 19:58:27 +02:00
/// <param name="operation">The name of the operation. If null, continues last operation</param>
2025-04-22 14:26:46 +02:00
/// <param name="shouldclose">Should the connection be closed when this object is disposed</param>
/// <param name="pagecachesize">The page cache size</param>
public LocalDatabase ( string path , string operation , bool shouldclose , long pagecachesize )
2025-05-12 16:58:23 +02:00
: this ( CreateConnection ( path , pagecachesize ). Await (), operation )
2013-03-08 22:24:54 +01:00
{
2016-04-06 20:40:34 +02:00
ShouldCloseConnection = shouldclose ;
2025-04-22 14:26:46 +02:00
m_pagecachesize = pagecachesize ;
2013-03-27 16:06:45 +01:00
}
2013-04-04 20:34:26 +02:00
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
public LocalDatabase ( LocalDatabase db )
2016-09-15 11:39:27 +02:00
: this ( db . m_connection )
{
2025-03-14 14:34:56 +01:00
OperationTimestamp = db . OperationTimestamp ;
m_connection = db . m_connection ;
m_operationid = db . m_operationid ;
2025-04-22 14:26:46 +02:00
m_pagecachesize = db . m_pagecachesize ;
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2013-03-27 16:06:45 +01:00
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
2023-09-21 19:58:27 +02:00
/// <param name="operation">The name of the operation. If null, continues last operation</param>
2025-05-12 16:58:23 +02:00
public LocalDatabase ( SqliteConnection connection , string operation )
2016-09-15 11:39:27 +02:00
: this ( connection )
2013-03-27 16:06:45 +01:00
{
2025-03-14 14:34:56 +01:00
OperationTimestamp = DateTime . UtcNow ;
2013-03-08 22:24:54 +01:00
m_connection = connection ;
2025-03-14 14:34:56 +01:00
if ( m_connection . State != ConnectionState . Open )
2013-03-27 16:06:45 +01:00
m_connection . Open ();
2023-09-21 19:58:27 +02:00
if ( operation != null )
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
using var transaction = m_connection . BeginTransaction ();
cmd . Transaction = transaction ;
cmd . CommandText = @"INSERT INTO ""Operation"" (""Description"", ""Timestamp"") VALUES (@Description, @Timestamp); SELECT last_insert_rowid();" ;
cmd . SetParameterValue ( "@Description" , operation );
cmd . SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( OperationTimestamp ));
m_operationid = cmd . ExecuteScalarInt64Async (- 1 ). Await ();
2023-09-21 19:58:27 +02:00
}
else
{
// Get last operation
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . CommandText = @"SELECT ""ID"", ""Timestamp"" FROM ""Operation"" ORDER BY ""Timestamp"" DESC LIMIT 1" ;
using var rd = cmd . ExecuteReader ( @"SELECT ""ID"", ""Timestamp"" FROM ""Operation"" ORDER BY ""Timestamp"" DESC LIMIT 1" );
if (! rd . Read ())
throw new Exception ( "LocalDatabase does not contain a previous operation." );
2025-03-18 22:41:11 +01:00
2025-05-12 16:58:23 +02:00
m_operationid = rd . ConvertValueToInt64 ( 0 );
OperationTimestamp = ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 ));
2023-09-21 19:58:27 +02:00
}
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
private LocalDatabase ( SqliteConnection connection )
2016-09-15 11:39:27 +02:00
{
2025-04-03 14:20:00 +02:00
m_connection = connection ;
2025-03-18 22:41:11 +01:00
m_insertlogCommand = connection . CreateCommand ( @"INSERT INTO ""LogData"" (""OperationID"", ""Timestamp"", ""Type"", ""Message"", ""Exception"") VALUES (@OperationID, @Timestamp, @Type, @Message, @Exception)" );
m_insertremotelogCommand = connection . CreateCommand ( @"INSERT INTO ""RemoteOperation"" (""OperationID"", ""Timestamp"", ""Operation"", ""Path"", ""Data"") VALUES (@OperationID, @Timestamp, @Operation, @Path, @Data)" );
m_updateremotevolumeCommand = connection . CreateCommand ( @"UPDATE ""Remotevolume"" SET ""OperationID"" = @OperationID, ""State"" = @State, ""Hash"" = @Hash, ""Size"" = @Size WHERE ""Name"" = @Name" );
2025-03-18 20:44:05 +01:00
m_selectremotevolumesCommand = connection . CreateCommand ( @"SELECT ""ID"", ""Name"", ""Type"", ""Size"", ""Hash"", ""State"", ""DeleteGraceTime"", ""ArchiveTime"" FROM ""Remotevolume""" );
2025-03-18 22:41:11 +01:00
m_selectremotevolumeCommand = connection . CreateCommand ( m_selectremotevolumesCommand . CommandText + @" WHERE ""Name"" = @Name" );
2025-03-14 14:34:56 +01:00
m_selectduplicateRemoteVolumesCommand = connection . CreateCommand ( FormatInvariant ( $@"SELECT DISTINCT ""Name"", ""State"" FROM ""Remotevolume"" WHERE ""Name"" IN (SELECT ""Name"" FROM ""Remotevolume"" WHERE ""State"" IN ('{RemoteVolumeState.Deleted.ToString()}', '{RemoteVolumeState.Deleting.ToString()}')) AND NOT ""State"" IN ('{RemoteVolumeState.Deleted.ToString()}', '{RemoteVolumeState.Deleting.ToString()}')" ));
2025-03-18 22:41:11 +01:00
m_removeremotevolumeCommand = connection . CreateCommand ( @"DELETE FROM ""Remotevolume"" WHERE ""Name"" = @Name AND (""DeleteGraceTime"" < @Now OR ""State"" != @State)" );
m_removedeletedremotevolumeCommand = connection . CreateCommand ( FormatInvariant ( $@"DELETE FROM ""Remotevolume"" WHERE ""State"" == '{RemoteVolumeState.Deleted.ToString()}' AND (""DeleteGraceTime"" < @Now OR LENGTH(""DeleteGraceTime"") > 12) " )); // >12 is to handle removal of old records that were in ticks
m_selectremotevolumeIdCommand = connection . CreateCommand ( @"SELECT ""ID"" FROM ""Remotevolume"" WHERE ""Name"" = @Name" );
2025-04-24 21:26:24 +02:00
m_createremotevolumeCommand = connection . CreateCommand ( @"INSERT INTO ""Remotevolume"" (""OperationID"", ""Name"", ""Type"", ""State"", ""Size"", ""VerificationCount"", ""DeleteGraceTime"", ""ArchiveTime"") VALUES (@OperationID, @Name, @Type, @State, @Size, @VerificationCount, @DeleteGraceTime, @ArchiveTime); SELECT last_insert_rowid();" );
2025-03-18 22:41:11 +01:00
m_insertIndexBlockLink = connection . CreateCommand ( @"INSERT INTO ""IndexBlockLink"" (""IndexVolumeID"", ""BlockVolumeID"") VALUES (@IndexVolumeId, @BlockVolumeId)" );
m_findpathprefixCommand = connection . CreateCommand ( @"SELECT ""ID"" FROM ""PathPrefix"" WHERE ""Prefix"" = @Prefix" );
m_insertpathprefixCommand = connection . CreateCommand ( @"INSERT INTO ""PathPrefix"" (""Prefix"") VALUES (@Prefix); SELECT last_insert_rowid(); " );
2013-05-25 16:40:15 +02:00
}
2019-08-05 20:14:05 -04:00
2013-07-16 14:51:59 +02:00
/// <summary>
/// Creates a DateTime instance by adding the specified number of seconds to the EPOCH value
2025-05-12 16:58:23 +02:00
/// </summary>
2013-07-16 14:51:59 +02:00
public static DateTime ParseFromEpochSeconds ( long seconds )
{
return Library . Utility . Utility . EPOCH . AddSeconds ( seconds );
}
2016-03-16 00:49:28 +01:00
2025-05-12 16:58:23 +02:00
public async Task UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , SqliteTransaction transaction )
2019-08-05 20:14:05 -04:00
{
2025-05-12 16:58:23 +02:00
await UpdateRemoteVolume ( name , state , size , hash , false , transaction );
2016-03-24 16:30:19 +01:00
}
2016-03-16 00:49:28 +01:00
2025-05-12 16:58:23 +02:00
public async Task UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , bool suppressCleanup , SqliteTransaction transaction )
2016-03-24 16:30:19 +01:00
{
2025-05-12 16:58:23 +02:00
await UpdateRemoteVolume ( name , state , size , hash , suppressCleanup , new TimeSpan ( 0 ), null , transaction );
2016-03-24 16:30:19 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , bool suppressCleanup , TimeSpan deleteGraceTime , bool? setArchived , SqliteTransaction transaction )
2013-03-27 16:06:45 +01:00
{
2013-04-09 20:43:27 +02:00
m_updateremotevolumeCommand . Transaction = transaction ;
2025-05-12 16:58:23 +02:00
m_updateremotevolumeCommand . SetParameterValue ( "@OperationID" , m_operationid );
m_updateremotevolumeCommand . SetParameterValue ( "@State" , state . ToString ());
m_updateremotevolumeCommand . SetParameterValue ( "@Hash" , hash );
m_updateremotevolumeCommand . SetParameterValue ( "@Size" , size );
m_updateremotevolumeCommand . SetParameterValue ( "@Name" , name );
var c = await m_updateremotevolumeCommand . ExecuteNonQueryAsync ();
2019-09-07 17:16:34 -04:00
2013-04-09 20:43:27 +02:00
if ( c != 1 )
2019-09-07 17:16:34 -04:00
{
throw new Exception ( $"Unexpected number of remote volumes detected: {c}!" );
}
2016-03-16 00:49:28 +01:00
2016-03-24 16:30:19 +01:00
if ( deleteGraceTime . Ticks > 0 )
2019-09-07 17:16:34 -04:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . CommandText = @"UPDATE ""RemoteVolume"" SET ""DeleteGraceTime"" = @DeleteGraceTime WHERE ""Name"" = @Name " ;
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@DeleteGraceTime" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow + deleteGraceTime ));
cmd . SetParameterValue ( "@Name" , name );
c = await cmd . ExecuteNonQueryAsync ();
2025-03-18 22:41:11 +01:00
2025-05-12 16:58:23 +02:00
if ( c != 1 )
throw new Exception ( $"Unexpected number of updates when recording remote volume updates: {c}!" );
2025-01-29 11:36:45 +01:00
}
2025-01-29 21:44:06 +01:00
if ( setArchived . HasValue )
2025-01-29 11:36:45 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . CommandText = @"UPDATE ""RemoteVolume"" SET ""ArchiveTime"" = @ArchiveTime WHERE ""Name"" = @Name " ;
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@ArchiveTime" , setArchived . Value ? Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ) : 0 );
cmd . SetParameterValue ( "@Name" , name );
c = await cmd . ExecuteNonQueryAsync ();
2025-04-23 22:37:31 +02:00
2025-05-12 16:58:23 +02:00
if ( c != 1 )
throw new Exception ( $"Unexpected number of updates when recording remote volume archive-time updates: {c}!" );
2019-09-07 17:16:34 -04:00
}
2016-03-24 16:30:19 +01:00
2016-03-16 00:49:28 +01:00
if (! suppressCleanup && state == RemoteVolumeState . Deleted )
2019-09-07 17:16:34 -04:00
{
2025-05-12 16:58:23 +02:00
await RemoveRemoteVolume ( name , transaction );
2019-09-07 17:16:34 -04:00
}
2013-03-27 16:06:45 +01:00
}
2024-06-21 07:52:07 +02:00
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < KeyValuePair < long , DateTime >> FilesetTimes ()
2019-08-05 20:14:05 -04:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . CommandText = @"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC" ;
using var rd = await cmd . ExecuteReaderAsync ();
while ( rd . Read ())
yield return new KeyValuePair < long , DateTime >( rd . ConvertValueToInt64 ( 0 ), ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime ());
2013-05-11 22:56:21 +02:00
}
2025-05-12 16:58:23 +02:00
public async Task <( string Query , Dictionary < string , object? > Values )> GetFilelistWhereClause ( DateTime time , long [] versions , IEnumerable < KeyValuePair < long , DateTime >>? filesetslist = null , bool singleTimeMatch = false )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
KeyValuePair < long , DateTime >[] filesets ;
if ( filesetslist != null )
filesets = [.. filesetslist ];
else
filesets = await FilesetTimes (). ToArrayAsync ();
2025-03-14 14:34:56 +01:00
var query = new StringBuilder ();
2025-04-03 14:20:00 +02:00
var args = new Dictionary < string , object? >();
2013-05-20 13:48:44 +02:00
if ( time . Ticks > 0 || ( versions != null && versions . Length > 0 ))
{
2013-06-20 20:17:10 +02:00
var hasTime = false ;
2013-05-20 13:48:44 +02:00
if ( time . Ticks > 0 )
{
if ( time . Kind == DateTimeKind . Unspecified )
throw new Exception ( "Invalid DateTime given, must be either local or UTC" );
2018-05-13 15:29:55 -07:00
2025-03-18 22:41:11 +01:00
query . Append ( singleTimeMatch ? @" ""Timestamp"" = @Timestamp" : @" ""Timestamp"" <= @Timestamp" );
2013-08-20 21:37:30 +02:00
// Make sure the resolution is the same (i.e. no milliseconds)
2025-03-18 22:41:11 +01:00
args . Add ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( time ));
2013-06-20 20:17:10 +02:00
hasTime = true ;
2013-05-20 13:48:44 +02:00
}
2018-05-13 15:29:55 -07:00
2013-05-20 13:48:44 +02:00
if ( versions != null && versions . Length > 0 )
{
2025-03-14 14:34:56 +01:00
var qs = new StringBuilder ();
2018-05-12 17:24:33 -07:00
foreach ( var v in versions )
{
2013-05-20 13:48:44 +02:00
if ( v >= 0 && v < filesets . Length )
{
2025-03-18 22:41:11 +01:00
var argName = "@Fileset" + v ;
args . Add ( argName , filesets [ v ]. Key );
qs . Append ( argName );
2025-05-12 16:58:23 +02:00
qs . Append ( ',' );
2013-05-20 13:48:44 +02:00
}
2013-08-06 22:57:03 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "SkipInvalidVersion" , null , "Skipping invalid version: {0}" , v );
2018-05-12 17:24:33 -07:00
}
2018-05-13 15:29:55 -07:00
2013-05-20 13:48:44 +02:00
if ( qs . Length > 0 )
{
2013-06-20 20:17:10 +02:00
if ( hasTime )
2018-05-13 15:29:55 -07:00
query . Append ( " OR " );
query . Append ( @" ""ID"" IN (" + qs . ToString ( 0 , qs . Length - 1 ) + ")" );
2013-05-20 13:48:44 +02:00
}
}
2013-08-06 22:57:03 +02:00
2018-05-13 15:29:55 -07:00
if ( query . Length > 0 )
{
query . Insert ( 0 , " WHERE " );
}
2013-05-20 13:48:44 +02:00
}
2019-08-05 20:14:05 -04:00
2025-03-18 22:41:11 +01:00
return ( query . ToString (), args );
2013-05-20 13:48:44 +02:00
}
2025-05-12 16:58:23 +02:00
public async Task < long > GetRemoteVolumeID ( string file , SqliteTransaction ? transaction = null )
2016-09-15 11:39:27 +02:00
{
m_selectremotevolumeIdCommand . Transaction = transaction ;
2025-05-12 16:58:23 +02:00
m_selectremotevolumeIdCommand . SetParameterValue ( "@Name" , file );
return await m_selectremotevolumeIdCommand . ExecuteScalarInt64Async (- 1 );
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < KeyValuePair < string , long >> GetRemoteVolumeIDs ( IEnumerable < string > files , SqliteTransaction ? transaction = null )
2024-11-01 14:50:26 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""Name"", ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" IN (@Name)" );
cmd . Transaction = transaction ;
using var tmptable = new TemporaryDbValueList ( m_connection , transaction , files );
cmd . ExpandInClauseParameter ( "@Name" , tmptable );
2024-11-01 14:50:26 +01:00
2025-05-12 16:58:23 +02:00
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
yield return new KeyValuePair < string , long >( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToInt64 ( 1 ));
2024-11-01 14:50:26 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task < RemoteVolumeEntry > GetRemoteVolume ( string file , SqliteTransaction ? transaction = null )
2013-03-27 16:06:45 +01:00
{
2016-02-22 21:27:12 +01:00
m_selectremotevolumeCommand . Transaction = transaction ;
2025-03-18 22:41:11 +01:00
m_selectremotevolumeCommand . SetParameterValue ( "@Name" , file );
2025-05-12 16:58:23 +02:00
using ( var rd = await m_selectremotevolumeCommand . ExecuteReaderAsync ())
if ( await rd . ReadAsync ())
2016-02-22 21:27:12 +01:00
return new RemoteVolumeEntry (
rd . ConvertValueToInt64 ( 0 ),
2025-04-03 14:20:00 +02:00
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 4 ),
2016-02-22 21:27:12 +01:00
rd . ConvertValueToInt64 ( 3 , - 1 ),
2025-04-03 14:20:00 +02:00
( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), rd . ConvertValueToString ( 2 ) ?? "" ),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . ConvertValueToString ( 5 ) ?? "" ),
2025-03-18 20:44:05 +01:00
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 6 , 0 )),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 7 , 0 ))
2016-02-22 21:27:12 +01:00
);
2019-08-05 20:14:05 -04:00
2016-02-22 21:27:12 +01:00
return RemoteVolumeEntry . Empty ;
2013-03-27 16:06:45 +01:00
}
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < KeyValuePair < string , RemoteVolumeState >> DuplicateRemoteVolumes ( SqliteTransaction ? transaction )
2016-03-24 16:31:07 +01:00
{
2025-05-12 16:58:23 +02:00
m_selectduplicateRemoteVolumesCommand . Transaction = transaction ;
await foreach ( var rd in m_selectduplicateRemoteVolumesCommand . ExecuteReaderEnumerableAsync ())
2016-03-24 16:31:07 +01:00
{
2016-03-30 01:30:51 +02:00
yield return new KeyValuePair < string , RemoteVolumeState >(
2025-04-03 14:20:00 +02:00
rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Name was null" ),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . ConvertValueToString ( 1 ) ?? "" )
2016-03-24 16:31:07 +01:00
);
}
}
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < RemoteVolumeEntry > GetRemoteVolumes ( SqliteTransaction ? transaction = null )
2013-03-27 16:06:45 +01:00
{
2016-02-22 21:27:12 +01:00
m_selectremotevolumesCommand . Transaction = transaction ;
2025-05-12 16:58:23 +02:00
using var rd = await m_selectremotevolumesCommand . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
2013-03-27 16:06:45 +01:00
{
2025-05-12 16:58:23 +02:00
yield return new RemoteVolumeEntry (
rd . ConvertValueToInt64 ( 0 ),
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 4 ),
rd . ConvertValueToInt64 ( 3 , - 1 ),
( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), rd . ConvertValueToString ( 2 ) ?? "" ),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . ConvertValueToString ( 5 ) ?? "" ),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 6 , 0 )),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 7 , 0 ))
);
2013-03-27 16:06:45 +01:00
}
}
/// <summary>
/// Log an operation performed on the remote backend
/// </summary>
/// <param name="operation">The operation performed</param>
/// <param name="path">The path involved</param>
/// <param name="data">Any data relating to the operation</param>
2025-05-12 16:58:23 +02:00
public async Task LogRemoteOperation ( string operation , string path , string? data , SqliteTransaction ? transaction )
{
m_insertremotelogCommand . Transaction = transaction ;
m_insertremotelogCommand . SetParameterValue ( "@OperationID" , m_operationid );
m_insertremotelogCommand . SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
m_insertremotelogCommand . SetParameterValue ( "@Operation" , operation );
m_insertremotelogCommand . SetParameterValue ( "@Path" , path );
m_insertremotelogCommand . SetParameterValue ( "@Data" , data );
await m_insertremotelogCommand . ExecuteNonQueryAsync ();
2013-03-27 16:06:45 +01:00
}
/// <summary>
/// Log a debug message
/// </summary>
/// <param name="type">The message type</param>
/// <param name="message">The message</param>
/// <param name="exception">An optional exception</param>
2025-05-12 16:58:23 +02:00
public async Task LogMessage ( string type , string message , Exception ? exception , SqliteTransaction ? transaction )
2013-03-27 16:06:45 +01:00
{
2025-05-12 16:58:23 +02:00
m_insertlogCommand . Transaction = transaction ;
m_insertlogCommand . SetParameterValue ( "@OperationID" , m_operationid );
m_insertlogCommand . SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
m_insertlogCommand . SetParameterValue ( "@Type" , type );
m_insertlogCommand . SetParameterValue ( "@Message" , message );
m_insertlogCommand . SetParameterValue ( "@Exception" , exception ?. ToString ());
await m_insertlogCommand . ExecuteNonQueryAsync ();
2013-03-27 16:06:45 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task UnlinkRemoteVolume ( string name , RemoteVolumeState state , SqliteTransaction transaction )
2016-03-24 16:31:07 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ( @"DELETE FROM ""RemoteVolume"" WHERE ""Name"" = @Name AND ""State"" = @State " );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@Name" , name );
cmd . SetParameterValue ( "@State" , state . ToString ());
var c = await cmd . ExecuteNonQueryAsync ();
2025-03-18 22:41:11 +01:00
2025-05-12 16:58:23 +02:00
if ( c != 1 )
throw new Exception ( $"Unexpected number of remote volumes deleted: {c}, expected {1}" );
2016-03-24 16:31:07 +01:00
2025-05-12 16:58:23 +02:00
await transaction . CommitAsync ();
2016-03-24 16:31:07 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task RemoveRemoteVolume ( string name , SqliteTransaction transaction )
2013-03-27 16:06:45 +01:00
{
2025-05-12 16:58:23 +02:00
await RemoveRemoteVolumes ([ name ], transaction );
2016-03-16 00:49:28 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task RemoveRemoteVolumes ( IEnumerable < string > names , SqliteTransaction transaction )
2016-03-16 00:49:28 +01:00
{
2016-12-24 11:42:26 +01:00
if ( names == null || ! names . Any ()) return ;
2016-03-16 00:49:28 +01:00
2025-05-12 16:58:23 +02:00
using var deletecmd = m_connection . CreateCommand ();
string temptransguid = Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
var volidstable = "DelVolSetIds-" + temptransguid ;
var blocksetidstable = "DelBlockSetIds-" + temptransguid ;
var filesetidstable = "DelFilesetIds-" + temptransguid ;
2016-03-16 00:49:28 +01:00
2025-05-12 16:58:23 +02:00
// Create and fill a temp table with the volids to delete. We avoid using too many parameters that way.
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMP TABLE ""{volidstable}"" (""ID"" INTEGER PRIMARY KEY)" ));
deletecmd . SetCommandAndParameters ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{volidstable}"" SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" IN (@VolumeNames)" ))
. ExpandInClauseParameter ( "@VolumeNames" , names . ToArray ())
. ExecuteNonQuery ();
2025-05-09 11:12:45 +02:00
2025-05-12 16:58:23 +02:00
var volIdsSubQuery = FormatInvariant ( $@"SELECT ""ID"" FROM ""{volidstable}"" " );
deletecmd . Parameters . Clear ();
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
var bsIdsSubQuery = FormatInvariant ( @ $"
2025-03-14 12:13:17 +01:00
SELECT DISTINCT ""BlocksetEntry"".""BlocksetID"" FROM ""BlocksetEntry"", ""Block""
2025-05-12 16:58:23 +02:00
WHERE ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""Block"".""VolumeID"" IN ({volIdsSubQuery})
UNION ALL
2025-03-14 12:13:17 +01:00
SELECT DISTINCT ""BlocksetID"" FROM ""BlocklistHash""
WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"" WHERE ""VolumeID"" IN ({volIdsSubQuery}))" );
2013-04-08 22:24:54 +02:00
2025-05-12 16:58:23 +02:00
// Create a temporary table to cache subquery result, as it might take long (SQLite does not cache at all).
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMP TABLE ""{blocksetidstable}"" (""ID"" INTEGER PRIMARY KEY)" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{blocksetidstable}"" (""ID"") {bsIdsSubQuery}" ));
bsIdsSubQuery = FormatInvariant ( $@"SELECT DISTINCT ""ID"" FROM ""{blocksetidstable}"" " );
deletecmd . Parameters . Clear ();
2016-03-13 13:23:06 +01:00
2025-05-12 16:58:23 +02:00
// Create a temp table to associate metadata that is being deleted to a fileset
var metadataFilesetQuery = FormatInvariant ( $@"SELECT Metadataset.ID, FilesetEntry.FilesetID
2020-02-23 13:23:21 -06:00
FROM Metadataset
INNER JOIN FileLookup ON FileLookup.MetadataID = Metadataset.ID
INNER JOIN FilesetEntry ON FilesetEntry.FileID = FileLookup.ID
WHERE Metadataset.BlocksetID IN ({bsIdsSubQuery})
2025-03-13 23:06:32 +01:00
OR Metadataset.ID IN (SELECT MetadataID FROM FileLookup WHERE BlocksetID IN ({bsIdsSubQuery}))" );
2020-02-23 13:23:21 -06:00
2025-05-12 16:58:23 +02:00
var metadataFilesetTable = @"DelMetadataFilesetIds-" + temptransguid ;
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMP TABLE ""{metadataFilesetTable}"" (MetadataID INTEGER PRIMARY KEY, FilesetID INTEGER)" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{metadataFilesetTable}"" (MetadataID, FilesetID) {metadataFilesetQuery}" ));
2020-02-23 13:23:21 -06:00
2025-05-12 16:58:23 +02:00
// Delete FilesetEntry rows that had their metadata deleted
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM FilesetEntry
2020-02-23 13:23:21 -06:00
WHERE FilesetEntry.FilesetID IN (SELECT DISTINCT FilesetID FROM ""{metadataFilesetTable}"")
AND FilesetEntry.FileID IN (
SELECT FilesetEntry.FileID
FROM FilesetEntry
INNER JOIN FileLookup ON FileLookup.ID = FilesetEntry.FileID
2025-03-13 23:06:32 +01:00
WHERE FileLookup.MetadataID IN (SELECT MetadataID FROM ""{metadataFilesetTable}""))" ));
2020-02-23 13:23:21 -06:00
2025-05-12 16:58:23 +02:00
// Delete FilesetEntry rows that had their blocks deleted
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM FilesetEntry WHERE FilesetEntry.FileID IN (
2020-02-23 13:23:21 -06:00
SELECT ID FROM FileLookup
2025-03-13 23:06:32 +01:00
WHERE FileLookup.BlocksetID IN ({bsIdsSubQuery}))" ));
2025-05-12 16:58:23 +02:00
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM FileLookup WHERE FileLookup.MetadataID IN (SELECT MetadataID FROM ""{metadataFilesetTable}"")" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""FileLookup"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""Blockset"" WHERE ""ID"" IN ({bsIdsSubQuery})" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""BlocksetEntry"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
await deletecmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""BlocklistHash"" WHERE ""BlocklistHash"".""BlocksetID"" IN ({bsIdsSubQuery})" ));
// If the volume is a block or index volume, this will update the crosslink table, otherwise nothing will happen
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""IndexBlockLink"" WHERE ""BlockVolumeID"" IN ({volIdsSubQuery}) OR ""IndexVolumeID"" IN ({volIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""Block"" WHERE ""VolumeID"" IN ({volIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""DeletedBlock"" WHERE ""VolumeID"" IN ({volIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""ChangeJournalData"" WHERE ""FilesetID"" IN (SELECT ""ID"" FROM ""Fileset"" WHERE ""VolumeID"" IN ({volIdsSubQuery}))" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM FilesetEntry WHERE FilesetID IN (SELECT ID FROM Fileset WHERE VolumeID IN ({volIdsSubQuery}))" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TABLE ""{filesetidstable}"" (""ID"" INTEGER PRIMARY KEY)" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{filesetidstable}"" SELECT ""ID"" FROM ""Fileset"" WHERE ""VolumeID"" IN ({volIdsSubQuery})" ));
// Delete from Fileset if FilesetEntry rows were deleted by related metadata and there are no references in FilesetEntry anymore
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{filesetidstable}"" SELECT ""ID"" FROM ""Fileset"" WHERE ""Fileset"".""ID"" IN
2025-05-14 23:36:48 +02:00
(SELECT DISTINCT ""FilesetID"" FROM ""{metadataFilesetTable}"")
AND ""Fileset"".""ID"" NOT IN
(SELECT DISTINCT ""FilesetID"" FROM FilesetEntry)" ));
2025-05-12 16:58:23 +02:00
// Since we are deleting the fileset, we also need to mark the remote volume as deleting so it will be cleaned up later
deletecmd . SetCommandAndParameters ( FormatInvariant ( $@"UPDATE ""RemoteVolume"" SET ""State"" = @NewState WHERE ""ID"" IN (SELECT DISTINCT ""VolumeID"" FROM ""Fileset"" WHERE ""Fileset"".""ID"" IN (SELECT ""ID"" FROM ""{filesetidstable}"")) AND ""State"" IN (@AllowedStates)" ))
. SetParameterValue ( "@NewState" , RemoteVolumeState . Deleting . ToString ())
. ExpandInClauseParameter ( "@AllowedStates" , [ RemoteVolumeState . Uploading . ToString (), RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString (), RemoteVolumeState . Temporary . ToString ()])
. ExecuteNonQuery ();
2025-05-14 23:36:48 +02:00
2025-05-12 16:58:23 +02:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""Fileset"" WHERE ""ID"" IN (SELECT ""ID"" FROM ""{filesetidstable}"")" ));
2025-05-14 23:36:48 +02:00
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
// Clean up temp tables for subqueries. We truncate content and then try to delete.
// Drop in try-block, as it fails in nested transactions (SQLite problem)
// SQLite.SQLiteException (0x80004005): database table is locked
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{blocksetidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{volidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{metadataFilesetTable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{filesetidstable}"" " ));
try
{
deletecmd . CommandTimeout = 2 ;
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{blocksetidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{volidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{metadataFilesetTable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{filesetidstable}"" " ));
}
catch { /* Ignore, will be deleted on close anyway. */ }
2018-12-03 08:13:58 -02:00
2025-05-12 16:58:23 +02:00
m_removeremotevolumeCommand . Transaction = transaction ;
m_removeremotevolumeCommand . SetParameterValue ( "@Now" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
m_removeremotevolumeCommand . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
foreach ( var name in names )
{
m_removeremotevolumeCommand . SetParameterValue ( "@Name" , name );
await m_removeremotevolumeCommand . ExecuteNonQueryAsync ();
}
2019-09-07 17:16:34 -04:00
2025-05-12 16:58:23 +02:00
// Validate before commiting changes
var nonAttachedFiles = await deletecmd . ExecuteScalarInt64Async ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup"")" );
if ( nonAttachedFiles > 0 )
throw new ConstraintException ( $"Detected {nonAttachedFiles} file(s) in FilesetEntry without corresponding FileLookup entry" );
2025-03-20 15:59:14 +01:00
2025-05-12 16:58:23 +02:00
await transaction . CommitAsync ();
2013-03-27 16:06:45 +01:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task Vacuum ()
2013-08-23 22:15:07 +02:00
{
2025-01-14 19:42:03 +01:00
m_hasExecutedVacuum = true ;
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
await cmd . ExecuteNonQueryAsync ( "VACUUM" );
2013-08-23 22:15:07 +02:00
}
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
public async Task < long > RegisterRemoteVolume ( string name , RemoteVolumeType type , long size , RemoteVolumeState state )
2015-04-05 14:33:13 +02:00
{
2025-05-12 16:58:23 +02:00
var transaction = m_connection . BeginTransaction ();
return await RegisterRemoteVolume ( name , type , state , size , new TimeSpan ( 0 ), transaction );
2015-04-05 14:33:13 +02:00
}
2025-05-12 16:58:23 +02:00
public async Task < long > RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , SqliteTransaction transaction )
2015-04-05 14:33:13 +02:00
{
2025-05-12 16:58:23 +02:00
return await RegisterRemoteVolume ( name , type , state , new TimeSpan ( 0 ), transaction );
2015-04-05 14:33:13 +02:00
}
2015-08-24 10:50:47 +01:00
2025-05-12 16:58:23 +02:00
public async Task < long > RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , TimeSpan deleteGraceTime , SqliteTransaction transaction )
2015-08-24 10:50:47 +01:00
{
2025-05-12 16:58:23 +02:00
return await RegisterRemoteVolume ( name , type , state , - 1 , deleteGraceTime , transaction );
2015-08-24 10:50:47 +01:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task < long > RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , long size , TimeSpan deleteGraceTime , SqliteTransaction transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
m_createremotevolumeCommand . Transaction = transaction ;
m_createremotevolumeCommand . SetParameterValue ( "@OperationId" , m_operationid );
m_createremotevolumeCommand . SetParameterValue ( "@Name" , name );
m_createremotevolumeCommand . SetParameterValue ( "@Type" , type . ToString ());
m_createremotevolumeCommand . SetParameterValue ( "@State" , state . ToString ());
m_createremotevolumeCommand . SetParameterValue ( "@Size" , size );
m_createremotevolumeCommand . SetParameterValue ( "@VerificationCount" , 0 );
m_createremotevolumeCommand . SetParameterValue ( "@DeleteGraceTime" , deleteGraceTime . Ticks <= 0 ? 0 : ( DateTime . UtcNow + deleteGraceTime ). Ticks );
m_createremotevolumeCommand . SetParameterValue ( "@ArchiveTime" , 0 );
var r = await m_createremotevolumeCommand . ExecuteScalarInt64Async ();
await transaction . CommitAsync ();
return r ;
2013-03-27 16:06:45 +01:00
}
2013-08-24 22:27:30 +02:00
2025-05-12 16:58:23 +02:00
public async Task < IEnumerable < long >> GetFilesetIDs ( DateTime restoretime , long [] versions , bool singleTimeMatch = false )
2013-08-24 22:27:30 +02:00
{
if ( restoretime . Kind == DateTimeKind . Unspecified )
throw new Exception ( "Invalid DateTime given, must be either local or UTC" );
2013-03-27 16:06:45 +01:00
2025-05-12 16:58:23 +02:00
( var query , var values ) = await GetFilelistWhereClause ( restoretime , versions , singleTimeMatch : singleTimeMatch );
2013-08-24 22:27:30 +02:00
var res = new List < long >();
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
using ( var rd = await cmd . ExecuteReaderAsync ( $@"SELECT ""ID"" FROM ""Fileset"" {query} ORDER BY ""Timestamp"" DESC" , values ))
while ( await rd . ReadAsync ())
res . Add ( rd . ConvertValueToInt64 ( 0 ));
if ( res . Count == 0 )
2019-08-05 20:14:05 -04:00
{
2025-05-12 16:58:23 +02:00
cmd . Parameters . Clear ();
using ( var rd = await cmd . ExecuteReaderAsync ( @"SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC " ))
while ( await rd . ReadAsync ())
2025-04-03 15:46:20 +02:00
res . Add ( rd . ConvertValueToInt64 ( 0 ));
2019-08-05 20:14:05 -04:00
2013-08-24 22:27:30 +02:00
if ( res . Count == 0 )
2025-05-12 16:58:23 +02:00
throw new Duplicati . Library . Interface . UserInformationException ( "No backup at the specified date" , "NoBackupAtDate" );
else
Logging . Log . WriteWarningMessage ( LOGTAG , "RestoreTimeNoMatch" , null , "Restore time or version did not match any existing backups, selecting newest backup" );
2013-03-27 16:06:45 +01:00
}
2025-05-12 16:58:23 +02:00
return res ;
2013-03-08 22:24:54 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task < IEnumerable < long >> FindMatchingFilesets ( DateTime restoretime , long [] versions )
2015-04-08 21:01:36 +02:00
{
if ( restoretime . Kind == DateTimeKind . Unspecified )
throw new Exception ( "Invalid DateTime given, must be either local or UTC" );
2025-05-12 16:58:23 +02:00
var ( query , args ) = await GetFilelistWhereClause ( restoretime , versions , singleTimeMatch : true );
2015-04-08 21:01:36 +02:00
var res = new List < long >();
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
2025-05-12 16:58:23 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ( @"SELECT ""ID"" FROM ""Fileset"" " + query + @" ORDER BY ""Timestamp"" DESC" , args ))
while ( await rd . ReadAsync ())
2025-04-03 15:46:20 +02:00
res . Add ( rd . ConvertValueToInt64 ( 0 ));
2015-04-08 21:01:36 +02:00
return res ;
}
2019-08-05 20:14:05 -04:00
2025-05-12 17:09:35 +02:00
public async Task < bool > IsFilesetFullBackup ( DateTime filesetTime , SqliteTransaction transaction )
2019-08-05 20:14:05 -04:00
{
2025-05-12 17:09:35 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( $@"SELECT ""IsFullBackup"" FROM ""Fileset"" WHERE ""Timestamp"" = @Timestamp" );
cmd . SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( filesetTime ));
using var rd = await cmd . ExecuteReaderAsync ();
if (! await rd . ReadAsync ())
return false ;
var isFullBackup = rd . GetInt32 ( 0 );
return isFullBackup == BackupType . FULL_BACKUP ;
2019-08-05 20:14:05 -04:00
}
2015-04-08 21:01:36 +02:00
2016-02-09 09:17:31 +01:00
// TODO: Remove this
2025-03-14 14:34:56 +01:00
public IDbTransaction BeginTransaction ()
2013-03-08 22:24:54 +01:00
{
2025-04-14 12:04:00 +02:00
return m_connection . BeginTransactionSafe ();
2013-03-08 22:24:54 +01:00
}
protected class TemporaryTransactionWrapper : IDisposable
{
2025-03-14 14:34:56 +01:00
private readonly IDbTransaction m_parent ;
2018-05-23 21:18:01 -07:00
private readonly bool m_isTemporary ;
2013-03-08 22:24:54 +01:00
2025-04-03 14:20:00 +02:00
public TemporaryTransactionWrapper ( IDbConnection connection , IDbTransaction ? transaction )
2013-03-08 22:24:54 +01:00
{
if ( transaction != null )
{
m_parent = transaction ;
m_isTemporary = false ;
}
else
{
2025-04-14 12:04:00 +02:00
m_parent = connection . BeginTransactionSafe ();
2013-03-08 22:24:54 +01:00
m_isTemporary = true ;
}
}
2019-08-05 20:14:05 -04:00
public void Commit ()
{
if ( m_isTemporary )
m_parent . Commit ();
2013-03-08 22:24:54 +01:00
}
2019-08-05 20:14:05 -04:00
public void Dispose ()
2013-03-08 22:24:54 +01:00
{
if ( m_isTemporary )
m_parent . Dispose ();
}
2025-03-14 14:34:56 +01:00
public IDbTransaction Parent { get { return m_parent ; } }
2013-03-27 16:06:45 +01:00
}
2019-08-05 20:14:05 -04:00
2025-04-03 14:20:00 +02:00
private IEnumerable < KeyValuePair < string , string >> GetDbOptionList ( IDbTransaction ? transaction = null )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
using var rd = await cmd . ExecuteReaderAsync ( @"SELECT ""Key"", ""Value"" FROM ""Configuration"" " );
while ( await rd . ReadAsync ())
yield return new KeyValuePair < string , string >( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToString ( 1 ) ?? "" );
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task < IDictionary < string , string >> GetDbOptions ( SqliteTransaction ? transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
var t = transaction ?? m_connection . BeginTransaction ();
var res = await GetDbOptionList ( t ). ToDictionaryAsync ( x => x . Key , x => x . Value );
if ( transaction == null )
await t . CommitAsync ();
return res ;
2016-09-15 11:39:27 +02:00
}
2016-03-18 13:26:12 +01:00
2025-03-07 15:24:56 +01:00
/// <summary>
/// Updates a database option
/// </summary>
/// <param name="key">The key to update</param>
/// <param name="value">The value to set</param>
2025-05-12 16:58:23 +02:00
private async Task UpdateDbOption ( string key , bool value )
2016-03-18 13:26:12 +01:00
{
2025-05-12 16:58:23 +02:00
var transaction = m_connection . BeginTransaction ();
var opts = await GetDbOptions ( transaction );
2016-03-18 13:26:12 +01:00
2025-03-06 12:02:11 +01:00
if ( value )
2025-03-06 14:53:46 +01:00
opts [ key ] = "true" ;
2025-03-06 12:02:11 +01:00
else
2025-03-06 14:53:46 +01:00
opts . Remove ( key );
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
await SetDbOptions ( opts , transaction );
await transaction . CommitAsync ();
2016-03-18 13:26:12 +01:00
}
2016-09-13 21:55:15 +02:00
2025-03-07 15:24:56 +01:00
/// <summary>
/// Flag indicating if a repair is in progress
/// </summary>
2025-03-06 12:02:11 +01:00
public bool RepairInProgress
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
get => GetDbOptions ( null ). Await (). ContainsKey ( "repair-in-progress" );
set => UpdateDbOption ( "repair-in-progress" , value ). Await ();
2025-03-06 12:02:11 +01:00
}
2016-09-15 11:39:27 +02:00
2025-03-07 15:24:56 +01:00
/// <summary>
/// Flag indicating if a repair is in progress
/// </summary>
2025-03-06 12:02:11 +01:00
public bool PartiallyRecreated
{
2025-05-12 16:58:23 +02:00
get => GetDbOptions ( null ). Await (). ContainsKey ( "partially-recreated" );
set => UpdateDbOption ( "partially-recreated" , value ). Await ();
2025-03-06 12:02:11 +01:00
}
2016-09-15 11:39:27 +02:00
2025-03-07 15:24:56 +01:00
/// <summary>
/// Flag indicating if the database can contain partial uploads
/// </summary>
2025-03-06 14:56:35 +01:00
public bool TerminatedWithActiveUploads
2025-03-06 12:02:11 +01:00
{
2025-05-12 16:58:23 +02:00
get => GetDbOptions ( null ). Await (). ContainsKey ( "terminated-with-active-uploads" );
set => UpdateDbOption ( "terminated-with-active-uploads" , value ). Await ();
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2025-03-07 15:24:56 +01:00
/// <summary>
/// Sets the database options
/// </summary>
/// <param name="options">The options to set</param>
/// <param name="transaction">An optional transaction</param>
2025-05-12 16:58:23 +02:00
public async Task SetDbOptions ( IDictionary < string , string > options , SqliteTransaction transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
await cmd . ExecuteNonQueryAsync ( @"DELETE FROM ""Configuration"" " );
foreach ( var kp in options )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( @"INSERT INTO ""Configuration"" (""Key"", ""Value"") VALUES (@Key, @Value) " );
cmd . SetParameterValue ( "@Key" , kp . Key );
cmd . SetParameterValue ( "@Value" , kp . Value );
await cmd . ExecuteNonQueryAsync ();
2016-09-15 11:39:27 +02:00
}
2025-05-12 16:58:23 +02:00
await transaction . CommitAsync ();
2016-09-15 11:39:27 +02:00
}
2025-05-12 16:58:23 +02:00
public async Task < long > GetBlocksLargerThan ( long fhblocksize )
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT COUNT(*) FROM ""Block"" WHERE ""Size"" > @Size" );
cmd . SetParameterValue ( "@Size" , fhblocksize );
return await cmd . ExecuteScalarInt64Async (- 1 );
2016-09-15 11:39:27 +02:00
}
2013-03-31 19:33:34 +02:00
2025-03-25 11:56:52 +01:00
/// <summary>
/// Verifies the consistency of the database
/// </summary>
/// <param name="blocksize">The block size in bytes</param>
/// <param name="hashsize">The hash size in byts</param>
/// <param name="verifyfilelists">Also verify filelists (can be slow)</param>
/// <param name="transaction">The transaction to run in</param>
2025-05-12 16:58:23 +02:00
public async Task VerifyConsistency ( long blocksize , long hashsize , bool verifyfilelists , SqliteTransaction transaction )
=> await VerifyConsistencyInner ( blocksize , hashsize , verifyfilelists , false , transaction );
2025-03-25 11:56:52 +01:00
/// <summary>
/// Verifies the consistency of the database prior to repair
/// </summary>
/// <param name="blocksize">The block size in bytes</param>
/// <param name="hashsize">The hash size in byts</param>
/// <param name="verifyfilelists">Also verify filelists (can be slow)</param>
/// <param name="transaction">The transaction to run in</param>
2025-05-12 16:58:23 +02:00
public async Task VerifyConsistencyForRepair ( long blocksize , long hashsize , bool verifyfilelists , SqliteTransaction transaction )
=> await VerifyConsistencyInner ( blocksize , hashsize , verifyfilelists , true , transaction );
2025-03-25 11:56:52 +01:00
/// <summary>
/// Verifies the consistency of the database
/// </summary>
/// <param name="blocksize">The block size in bytes</param>
/// <param name="hashsize">The hash size in byts</param>
/// <param name="verifyfilelists">Also verify filelists (can be slow)</param>
/// <param name="laxVerifyForRepair">Disable verify for errors that will be fixed by repair</param>
/// <param name="transaction">The transaction to run in</param>
2025-05-12 16:58:23 +02:00
private async Task VerifyConsistencyInner ( long blocksize , long hashsize , bool verifyfilelists , bool laxVerifyForRepair , SqliteTransaction transaction )
2013-04-21 20:00:37 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
// Calculate the lengths for each blockset
var combinedLengths = @"
SELECT
""A"".""ID"" AS ""BlocksetID"",
IFNULL(""B"".""CalcLen"", 0) AS ""CalcLen"",
2016-04-04 22:47:16 +02:00
""A"".""Length""
FROM
""Blockset"" A
LEFT OUTER JOIN
(
2025-05-12 16:58:23 +02:00
SELECT
2016-04-04 22:47:16 +02:00
""BlocksetEntry"".""BlocksetID"",
SUM(""Block"".""Size"") AS ""CalcLen""
FROM
""BlocksetEntry""
LEFT OUTER JOIN
""Block""
ON
""Block"".""ID"" = ""BlocksetEntry"".""BlockID""
GROUP BY ""BlocksetEntry"".""BlocksetID""
) B
ON
""A"".""ID"" = ""B"".""BlocksetID""
" ;
2025-05-12 16:58:23 +02:00
// For each blockset with wrong lengths, fetch the file path
var reportDetails = @"SELECT ""CalcLen"", ""Length"", ""A"".""BlocksetID"", ""File"".""Path"" FROM (" + combinedLengths + @") A, ""File"" WHERE ""A"".""BlocksetID"" = ""File"".""BlocksetID"" AND ""A"".""CalcLen"" != ""A"".""Length"" " ;
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ( reportDetails ))
if ( await rd . ReadAsync ())
{
var sb = new StringBuilder ();
sb . AppendLine ( "Found inconsistency in the following files while validating database: " );
var c = 0 ;
do
2016-09-15 11:39:27 +02:00
{
2025-05-12 16:58:23 +02:00
if ( c < 5 )
sb . AppendFormat ( "{0}, actual size {1}, dbsize {2}, blocksetid: {3}{4}" , rd . GetValue ( 3 ), rd . GetValue ( 1 ), rd . GetValue ( 0 ), rd . GetValue ( 2 ), Environment . NewLine );
c ++;
} while ( await rd . ReadAsync ());
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
c -= 5 ;
if ( c > 0 )
sb . AppendFormat ( "... and {0} more" , c );
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
sb . Append ( ". Run repair to fix it." );
throw new DatabaseInconsistencyException ( sb . ToString ());
}
2014-12-30 16:13:31 +01:00
2025-05-12 16:58:23 +02:00
var real_count = await cmd . ExecuteScalarInt64Async ( @"SELECT Count(*) FROM ""BlocklistHash""" , 0 );
var unique_count = await cmd . ExecuteScalarInt64Async ( @"SELECT Count(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")" , 0 );
2014-12-30 16:13:31 +01:00
2025-05-12 16:58:23 +02:00
if ( real_count != unique_count )
throw new DatabaseInconsistencyException ( $"Found {real_count} blocklist hashes, but there should be {unique_count}. Run repair to fix it." );
2015-02-03 23:47:33 +01:00
2025-05-12 16:58:23 +02:00
var itemswithnoblocklisthash = await cmd . ExecuteScalarInt64Async ( FormatInvariant ( $@"SELECT COUNT(*) FROM (SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {blocksize / hashsize} - 1) / {blocksize / hashsize}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual"")" ), 0 );
if ( itemswithnoblocklisthash != 0 )
throw new DatabaseInconsistencyException ( $"Found {itemswithnoblocklisthash} file(s) with missing blocklist hashes" );
2015-02-03 23:47:33 +01:00
2025-05-12 16:58:23 +02:00
if ( await cmd . ExecuteScalarInt64Async ( @"SELECT COUNT(*) FROM ""Blockset"" WHERE ""Length"" > 0 AND ""ID"" NOT IN (SELECT ""BlocksetId"" FROM ""BlocksetEntry"")" ) != 0 )
throw new DatabaseInconsistencyException ( "Detected non-empty blocksets with no associated blocks!" );
2016-03-30 12:29:22 +02:00
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""FileLookup"" WHERE ""BlocksetID"" != @FolderBlocksetId AND ""BlocksetID"" != @SymlinkBlocksetId AND NOT ""BlocksetID"" IN (SELECT ""ID"" FROM ""Blockset"")" );
cmd . SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID );
cmd . SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID );
if ( await cmd . ExecuteScalarInt64Async ( 0 ) != 0 )
throw new DatabaseInconsistencyException ( "Detected files associated with non-existing blocksets!" );
2016-04-04 18:11:48 +02:00
2025-05-12 16:58:23 +02:00
if (! laxVerifyForRepair )
{
cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State)" );
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
var filesetsMissingVolumes = await cmd . ExecuteScalarInt64Async ( 0 );
2016-03-30 12:29:22 +02:00
2025-05-12 16:58:23 +02:00
if ( filesetsMissingVolumes != 0 )
{
if ( filesetsMissingVolumes == 1 )
2025-03-25 11:56:52 +01:00
{
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Timestamp"", ""VolumeID"" FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State)" );
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
using var reader = await cmd . ExecuteReaderAsync ();
if ( await reader . ReadAsync ())
throw new DatabaseInconsistencyException ( $"Detected 1 fileset with missing volume: FilesetId = {reader.ConvertValueToInt64(0)}, Time = ({ParseFromEpochSeconds(reader.ConvertValueToInt64(1))}), unmatched VolumeID {reader.ConvertValueToInt64(2)}" );
2025-03-25 11:56:52 +01:00
}
2025-03-10 14:41:36 +01:00
2025-05-12 16:58:23 +02:00
throw new DatabaseInconsistencyException ( $"Detected {filesetsMissingVolumes} filesets with missing volumes" );
}
cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" );
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
var volumesMissingFilests = await cmd . ExecuteScalarInt64Async ( 0 );
if ( volumesMissingFilests != 0 )
{
if ( volumesMissingFilests == 1 )
2025-03-25 11:56:52 +01:00
{
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Name"", ""State"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" );
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
using var reader = await cmd . ExecuteReaderAsync ();
if ( await reader . ReadAsync ())
throw new DatabaseInconsistencyException ( $"Detected 1 volume with missing filesets: VolumeId = {reader.ConvertValueToInt64(0)}, Name = {reader.ConvertValueToString(1)}, State = {reader.ConvertValueToString(2)}" );
2025-03-25 11:56:52 +01:00
}
2025-05-12 16:58:23 +02:00
throw new DatabaseInconsistencyException ( $"Detected {volumesMissingFilests} volumes with missing filesets" );
2025-03-10 14:41:36 +01:00
}
2025-05-12 16:58:23 +02:00
}
2025-03-10 14:41:36 +01:00
2025-05-12 16:58:23 +02:00
var nonAttachedFiles = await cmd . ExecuteScalarInt64Async ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup"")" );
if ( nonAttachedFiles != 0 )
{
// Attempt to create a better error message by finding the first 10 fileset ids with the issue
using var filesetIdReader = await cmd . ExecuteReaderAsync ( @"SELECT DISTINCT(FilesetID) FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup"") LIMIT 11" );
var filesetIds = new HashSet < long >();
var overflow = false ;
while ( await filesetIdReader . ReadAsync ())
2025-03-20 15:59:14 +01:00
{
2025-05-12 16:58:23 +02:00
if ( filesetIds . Count >= 10 )
2025-03-20 15:59:14 +01:00
{
2025-05-12 16:58:23 +02:00
overflow = true ;
break ;
2025-03-20 15:59:14 +01:00
}
2025-05-12 16:58:23 +02:00
filesetIds . Add ( filesetIdReader . ConvertValueToInt64 ( 0 ));
}
2025-03-20 15:59:14 +01:00
2025-05-12 16:58:23 +02:00
var pairs = FilesetTimes ()
. Select (( x , i ) => new { FilesetId = x . Key , Version = i , Time = x . Value })
. Where ( x => filesetIds . Contains ( x . FilesetId ))
. Select ( x => $"Fileset {x.Version}: {x.Time} (id = {x.FilesetId})" );
2025-03-20 15:59:14 +01:00
2025-05-12 16:58:23 +02:00
// Fall back to a generic error message if we can't find the fileset ids
if (! await pairs . AnyAsync ())
throw new DatabaseInconsistencyException ( $"Detected {nonAttachedFiles} file(s) in FilesetEntry without corresponding FileLookup entry" );
2025-03-20 15:59:14 +01:00
2025-05-12 16:58:23 +02:00
if ( overflow )
pairs = pairs . Append ( "... and more" );
2025-03-20 15:59:14 +01:00
2025-05-12 16:58:23 +02:00
throw new DatabaseInconsistencyException ( $"Detected {nonAttachedFiles} file(s) in FilesetEntry without corresponding FileLookup entry in the following filesets:{Environment.NewLine}{string.Join(Environment.NewLine, pairs)}" );
}
2016-04-04 18:11:48 +02:00
2025-05-12 16:58:23 +02:00
if ( verifyfilelists )
{
var anyError = new List < string >();
using ( var cmd2 = m_connection . CreateCommand ())
2016-04-04 18:11:48 +02:00
{
2025-05-12 16:58:23 +02:00
cmd2 . Transaction = transaction ;
await foreach ( var filesetid in cmd . ExecuteReaderEnumerableAsync ( @"SELECT ""ID"" FROM ""Fileset"" " ). Select ( x => x . ConvertValueToInt64 ( 0 , - 1 )))
2025-03-10 21:02:16 +01:00
{
2025-05-12 16:58:23 +02:00
var expandedCmd = FormatInvariant ( $@"SELECT COUNT(*) FROM (SELECT DISTINCT ""Path"" FROM ({LocalDatabase.LIST_FILESETS}) UNION SELECT DISTINCT ""Path"" FROM ({LocalDatabase.LIST_FOLDERS_AND_SYMLINKS}))" );
cmd2 . SetCommandAndParameters ( expandedCmd );
cmd2 . SetParameterValue ( "@FilesetId" , filesetid );
cmd2 . SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID );
cmd2 . SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID );
var expandedlist = await cmd . ExecuteScalarInt64Async ( 0 );
//var storedfilelist = cmd2.ExecuteScalarInt64(FormatInvariant(@"SELECT COUNT(*) FROM ""FilesetEntry"", ""FileLookup"" WHERE ""FilesetEntry"".""FilesetID"" = @FilesetId AND ""FileLookup"".""ID"" = ""FilesetEntry"".""FileID"" AND ""FileLookup"".""BlocksetID"" != @FolderBlocksetId AND ""FileLookup"".""BlocksetID"" != @SymlinkBlocksetId"), 0, filesetid, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID);
cmd2 . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FilesetEntry"".""FilesetID"" = @FilesetId" );
cmd2 . SetParameterValue ( "@FilesetId" , filesetid );
var storedlist = await cmd2 . ExecuteScalarInt64Async ( 0 );
if ( expandedlist != storedlist )
2018-10-17 14:37:23 +02:00
{
2025-05-12 16:58:23 +02:00
var filesetname = filesetid . ToString ();
var fileset = await FilesetTimes (). Zip ( AsyncEnumerable . Range ( 0 , await FilesetTimes (). CountAsync ()), ( a , b ) => new Tuple < long , long , DateTime >( b , a . Key , a . Value )). FirstOrDefaultAsync ( x => x . Item2 == filesetid );
if ( fileset != null )
filesetname = $"version {fileset.Item1}: {fileset.Item3} (database id: {fileset.Item2})" ;
anyError . Add ( $"Unexpected difference in fileset {filesetname}, found {expandedlist} entries, but expected {storedlist}" );
2018-10-17 14:37:23 +02:00
}
2025-03-10 21:02:16 +01:00
}
2025-05-12 16:58:23 +02:00
}
if ( anyError . Any ())
{
throw new DatabaseInconsistencyException ( string . Join ( "\n\r" , anyError ), "FilesetDifferences" );
2016-04-04 18:11:48 +02:00
}
2013-04-21 20:00:37 +02:00
}
}
2019-08-05 20:14:05 -04:00
public interface IBlock
{
string Hash { get ; }
long Size { get ; }
}
2025-05-12 16:58:23 +02:00
internal class Block ( string hash , long size ) : IBlock
2019-08-05 20:14:05 -04:00
{
2025-05-12 16:58:23 +02:00
public string Hash { get ; private set ; } = hash ;
public long Size { get ; private set ; } = size ;
2019-08-05 20:14:05 -04:00
}
2013-04-27 15:13:14 +02:00
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < IBlock > GetBlocks ( long volumeid , SqliteTransaction transaction )
2019-08-05 20:14:05 -04:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT DISTINCT ""Hash"", ""Size"" FROM ""Block"" WHERE ""VolumeID"" = @VolumeId" );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@VolumeId" , volumeid );
using var rd = await cmd . ExecuteReaderAsync ();
while ( await rd . ReadAsync ())
yield return new Block ( rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Hash is null" ), rd . ConvertValueToInt64 ( 1 ));
2016-09-15 11:39:27 +02:00
}
2013-04-27 15:13:14 +02:00
2025-04-03 14:20:00 +02:00
// TODO: Replace this with an enumerable method
2013-04-27 15:13:14 +02:00
private class BlocklistHashEnumerable : IEnumerable < string >
{
private class BlocklistHashEnumerator : IEnumerator < string >
{
2025-05-13 08:40:07 +02:00
private readonly SqliteDataReader m_reader ;
2018-05-23 21:18:01 -07:00
private readonly BlocklistHashEnumerable m_parent ;
2025-04-03 14:20:00 +02:00
private string? m_path = null ;
2013-04-27 15:13:14 +02:00
private bool m_first = true ;
2025-04-03 14:20:00 +02:00
private string? m_current = null ;
2013-04-27 15:13:14 +02:00
2025-05-13 08:40:07 +02:00
public BlocklistHashEnumerator ( BlocklistHashEnumerable parent , SqliteDataReader reader )
2013-04-27 15:13:14 +02:00
{
m_reader = reader ;
m_parent = parent ;
}
2025-04-03 14:20:00 +02:00
public string Current { get { return m_current !; } }
2013-04-27 15:13:14 +02:00
public void Dispose ()
{
}
2025-03-14 14:34:56 +01:00
object System . Collections . IEnumerator . Current { get { return Current ; } }
2013-04-27 15:13:14 +02:00
public bool MoveNext ()
{
m_first = false ;
if ( m_path == null )
{
2025-04-03 14:20:00 +02:00
m_path = m_reader . ConvertValueToString ( 0 );
m_current = m_reader . ConvertValueToString ( 6 );
2013-04-27 15:13:14 +02:00
return true ;
}
else
{
if ( m_current == null )
return false ;
2025-05-13 08:40:07 +02:00
if (! m_reader . ReadAsync (). Await ())
2013-04-27 15:13:14 +02:00
{
m_current = null ;
m_parent . MoreData = false ;
return false ;
}
2025-04-03 14:20:00 +02:00
var np = m_reader . ConvertValueToString ( 0 );
2013-04-27 15:13:14 +02:00
if ( m_path != np )
{
m_current = null ;
return false ;
}
2025-04-03 14:20:00 +02:00
m_current = m_reader . ConvertValueToString ( 6 );
2013-04-27 15:13:14 +02:00
return true ;
}
}
public void Reset ()
{
if (! m_first )
throw new Exception ( "Iterator reset not supported" );
m_first = false ;
}
}
2025-05-13 08:40:07 +02:00
private readonly SqliteDataReader m_reader ;
2013-04-27 15:13:14 +02:00
2025-05-13 08:40:07 +02:00
public BlocklistHashEnumerable ( SqliteDataReader reader )
2013-04-27 15:13:14 +02:00
{
m_reader = reader ;
2025-03-14 14:34:56 +01:00
MoreData = true ;
2013-04-27 15:13:14 +02:00
}
public bool MoreData { get ; protected set ; }
public IEnumerator < string > GetEnumerator ()
{
return new BlocklistHashEnumerator ( this , m_reader );
}
System . Collections . IEnumerator System . Collections . IEnumerable . GetEnumerator ()
{
2025-03-14 14:34:56 +01:00
return GetEnumerator ();
2013-04-27 15:13:14 +02:00
}
}
2016-04-04 18:11:48 +02:00
public const string LIST_FILESETS = @"
SELECT
2025-05-12 16:58:23 +02:00
""L"".""Path"",
""L"".""Lastmodified"",
""L"".""Filelength"",
""L"".""Filehash"",
""L"".""Metahash"",
2016-04-04 18:11:48 +02:00
""L"".""Metalength"",
2025-05-12 16:58:23 +02:00
""L"".""BlocklistHash"",
2016-04-04 18:11:48 +02:00
""L"".""FirstBlockHash"",
""L"".""FirstBlockSize"",
""L"".""FirstMetaBlockHash"",
""L"".""FirstMetaBlockSize"",
""M"".""Hash"" AS ""MetaBlocklistHash""
FROM
(
2025-05-12 16:58:23 +02:00
SELECT
""J"".""Path"",
""J"".""Lastmodified"",
""J"".""Filelength"",
""J"".""Filehash"",
""J"".""Metahash"",
2016-04-04 18:11:48 +02:00
""J"".""Metalength"",
2025-05-12 16:58:23 +02:00
""K"".""Hash"" AS ""BlocklistHash"",
2016-04-04 18:11:48 +02:00
""J"".""FirstBlockHash"",
""J"".""FirstBlockSize"",
""J"".""FirstMetaBlockHash"",
""J"".""FirstMetaBlockSize"",
""J"".""MetablocksetID""
2025-05-12 16:58:23 +02:00
FROM
2016-04-04 18:11:48 +02:00
(
2025-05-12 16:58:23 +02:00
SELECT
""A"".""Path"" AS ""Path"",
""D"".""Lastmodified"" AS ""Lastmodified"",
""B"".""Length"" AS ""Filelength"",
""B"".""FullHash"" AS ""Filehash"",
""E"".""FullHash"" AS ""Metahash"",
2017-08-12 14:09:55 +01:00
""E"".""Length"" AS ""Metalength"",
""A"".""BlocksetID"" AS ""BlocksetID"",
""F"".""Hash"" AS ""FirstBlockHash"",
""F"".""Size"" AS ""FirstBlockSize"",
""H"".""Hash"" AS ""FirstMetaBlockHash"",
""H"".""Size"" AS ""FirstMetaBlockSize"",
""C"".""BlocksetID"" AS ""MetablocksetID""
2025-05-12 16:58:23 +02:00
FROM
""File"" A
2017-08-12 14:09:55 +01:00
LEFT JOIN ""Blockset"" B
2025-05-12 16:58:23 +02:00
ON ""A"".""BlocksetID"" = ""B"".""ID""
LEFT JOIN ""Metadataset"" C
2017-08-12 14:09:55 +01:00
ON ""A"".""MetadataID"" = ""C"".""ID""
LEFT JOIN ""FilesetEntry"" D
ON ""A"".""ID"" = ""D"".""FileID""
LEFT JOIN ""Blockset"" E
ON ""E"".""ID"" = ""C"".""BlocksetID""
LEFT JOIN ""BlocksetEntry"" G
ON ""B"".""ID"" = ""G"".""BlocksetID""
2025-05-12 16:58:23 +02:00
LEFT JOIN ""Block"" F
ON ""G"".""BlockID"" = ""F"".""ID""
2017-08-12 14:09:55 +01:00
LEFT JOIN ""BlocksetEntry"" I
ON ""E"".""ID"" = ""I"".""BlocksetID""
2025-05-12 16:58:23 +02:00
LEFT JOIN ""Block"" H
2017-08-12 14:09:55 +01:00
ON ""I"".""BlockID"" = ""H"".""ID""
2025-05-12 16:58:23 +02:00
WHERE
2017-08-12 14:09:55 +01:00
""A"".""BlocksetId"" >= 0 AND
2025-03-18 22:41:11 +01:00
""D"".""FilesetID"" = @FilesetId AND
2025-05-12 16:58:23 +02:00
(""I"".""Index"" = 0 OR ""I"".""Index"" IS NULL) AND
2017-08-12 14:09:55 +01:00
(""G"".""Index"" = 0 OR ""G"".""Index"" IS NULL)
2016-04-04 18:11:48 +02:00
) J
2025-05-12 16:58:23 +02:00
LEFT OUTER JOIN
""BlocklistHash"" K
ON
""K"".""BlocksetID"" = ""J"".""BlocksetID""
2016-04-04 18:11:48 +02:00
ORDER BY ""J"".""Path"", ""K"".""Index""
) L
LEFT OUTER JOIN
""BlocklistHash"" M
ON
""M"".""BlocksetID"" = ""L"".""MetablocksetID""
" ;
public const string LIST_FOLDERS_AND_SYMLINKS = @"
SELECT
""G"".""BlocksetID"",
""G"".""ID"",
""G"".""Path"",
""G"".""Length"",
""G"".""FullHash"",
""G"".""Lastmodified"",
""G"".""FirstMetaBlockHash"",
""H"".""Hash"" AS ""MetablocklistHash""
FROM
(
SELECT
""B"".""BlocksetID"",
""B"".""ID"",
""B"".""Path"",
""D"".""Length"",
""D"".""FullHash"",
""A"".""Lastmodified"",
""F"".""Hash"" AS ""FirstMetaBlockHash"",
""C"".""BlocksetID"" AS ""MetaBlocksetID""
FROM
2025-05-12 16:58:23 +02:00
""FilesetEntry"" A,
""File"" B,
""Metadataset"" C,
2016-04-04 18:11:48 +02:00
""Blockset"" D,
""BlocksetEntry"" E,
""Block"" F
2025-05-12 16:58:23 +02:00
WHERE
""A"".""FileID"" = ""B"".""ID""
AND ""B"".""MetadataID"" = ""C"".""ID""
AND ""C"".""BlocksetID"" = ""D"".""ID""
2016-04-04 18:11:48 +02:00
AND ""E"".""BlocksetID"" = ""C"".""BlocksetID""
AND ""E"".""BlockID"" = ""F"".""ID""
AND ""E"".""Index"" = 0
2025-03-18 22:41:11 +01:00
AND (""B"".""BlocksetID"" = @FolderBlocksetId OR ""B"".""BlocksetID"" = @SymlinkBlocksetId)
AND ""A"".""FilesetID"" = @FilesetId
2016-04-04 18:11:48 +02:00
) G
LEFT OUTER JOIN
""BlocklistHash"" H
ON
""H"".""BlocksetID"" = ""G"".""MetaBlocksetID""
ORDER BY
""G"".""Path"", ""H"".""Index""
" ;
2025-05-12 16:58:23 +02:00
public async Task WriteFileset ( Volumes . FilesetVolumeWriter filesetvolume , long filesetId , SqliteTransaction transaction )
2013-04-27 15:13:14 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( LIST_FOLDERS_AND_SYMLINKS );
cmd . SetParameterValue ( "@FilesetId" , filesetId );
cmd . SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID );
cmd . SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID );
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
string? lastpath = null ;
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
{
var blocksetID = rd . ConvertValueToInt64 ( 0 , - 1 );
var path = rd . ConvertValueToString ( 2 );
var metalength = rd . ConvertValueToInt64 ( 3 , - 1 );
var metahash = rd . ConvertValueToString ( 4 );
var metablockhash = rd . ConvertValueToString ( 6 );
var metablocklisthash = rd . ConvertValueToString ( 7 );
if ( path == lastpath )
Logging . Log . WriteWarningMessage ( LOGTAG , "DuplicatePathFound" , null , "Duplicate path detected: {0}" , path );
lastpath = path ;
if ( blocksetID == FOLDER_BLOCKSET_ID )
filesetvolume . AddDirectory ( path , metahash , metalength , metablockhash , string . IsNullOrWhiteSpace ( metablocklisthash ) ? null : new string [] { metablocklisthash });
else if ( blocksetID == SYMLINK_BLOCKSET_ID )
filesetvolume . AddSymlink ( path , metahash , metalength , metablockhash , string . IsNullOrWhiteSpace ( metablocklisthash ) ? null : new string [] { metablocklisthash });
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
// TODO: Perhaps run the above query after recreate and compare count(*) with count(*) from filesetentry where id = x
2013-04-27 15:13:14 +02:00
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( LIST_FILESETS );
cmd . SetParameterValue ( "@FilesetId" , filesetId );
2013-04-27 15:13:14 +02:00
2025-05-12 16:58:23 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
if ( await rd . ReadAsync ())
{
var more = false ;
do
2013-04-27 15:13:14 +02:00
{
2025-05-12 16:58:23 +02:00
var path = rd . ConvertValueToString ( 0 );
var filehash = rd . ConvertValueToString ( 3 );
var size = rd . ConvertValueToInt64 ( 2 );
var lastmodified = new DateTime ( rd . ConvertValueToInt64 ( 1 , 0 ), DateTimeKind . Utc );
var metahash = rd . ConvertValueToString ( 4 );
var metasize = rd . ConvertValueToInt64 ( 5 , - 1 );
var p = rd . GetValue ( 6 );
var blrd = ( p == null || p == DBNull . Value ) ? null : new BlocklistHashEnumerable ( rd );
var blockhash = rd . ConvertValueToString ( 7 );
var blocksize = rd . ConvertValueToInt64 ( 8 , - 1 );
var metablockhash = rd . ConvertValueToString ( 9 );
//var metablocksize = rd.ConvertValueToInt64(10, -1);
var metablocklisthash = rd . ConvertValueToString ( 11 );
if ( blockhash == filehash )
blockhash = null ;
if ( metablockhash == metahash )
metablockhash = null ;
filesetvolume . AddFile ( path , filehash , size , lastmodified , metahash , metasize , metablockhash , blockhash , blocksize , blrd , string . IsNullOrWhiteSpace ( metablocklisthash ) ? null : new string [] { metablocklisthash });
if ( blrd == null )
more = await rd . ReadAsync ();
else
more = blrd . MoreData ;
} while ( more );
}
2013-04-27 15:13:14 +02:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task LinkFilesetToVolume ( long filesetid , long volumeid , SqliteTransaction transaction )
2025-03-10 14:37:30 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"UPDATE ""Fileset"" SET ""VolumeID"" = @VolumeId WHERE ""ID"" = @FilesetId" );
cmd . SetParameterValue ( "@VolumeId" , volumeid );
cmd . SetParameterValue ( "@FilesetId" , filesetid );
var c = await cmd . ExecuteNonQueryAsync ();
2025-03-18 22:41:11 +01:00
2025-05-12 16:58:23 +02:00
if ( c != 1 )
throw new Exception ( $"Failed to link filesetid {filesetid} to volumeid {volumeid}" );
2025-03-10 14:37:30 +01:00
}
2025-05-12 16:58:23 +02:00
public async Task PushTimestampChangesToPreviousVersion ( long filesetId , SqliteTransaction transaction )
2025-02-06 15:31:20 +01:00
{
2025-03-18 22:41:11 +01:00
var query = @"
2025-02-06 15:31:20 +01:00
UPDATE FilesetEntry AS oldVersion
SET Lastmodified = tempVersion.Lastmodified
FROM FilesetEntry AS tempVersion
WHERE oldVersion.FileID = tempVersion.FileID
2025-03-18 22:41:11 +01:00
AND tempVersion.FilesetID = @FilesetId
AND oldVersion.FilesetID = (SELECT ID FROM Fileset WHERE ID != @FilesetId ORDER BY Timestamp DESC LIMIT 1)" ;
2025-02-06 15:31:20 +01:00
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ( query );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@FilesetId" , filesetId );
await cmd . ExecuteNonQueryAsync ();
2025-02-06 15:31:20 +01:00
}
2013-05-11 12:03:15 +02:00
/// <summary>
2019-09-07 17:16:34 -04:00
/// Keeps a list of filenames in a temporary table with a single column Path
2025-05-12 16:58:23 +02:00
/// </summary>
2013-05-11 12:03:15 +02:00
public class FilteredFilenameTable : IDisposable
{
public string Tablename { get ; private set ; }
2025-05-12 16:58:23 +02:00
private readonly SqliteConnection m_connection ;
2018-05-23 21:18:01 -07:00
2025-05-12 16:58:23 +02:00
public FilteredFilenameTable ( SqliteConnection connection , IFilter filter , SqliteTransaction transaction )
2013-05-11 12:03:15 +02:00
{
m_connection = connection ;
Tablename = "Filenames-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-03-14 14:34:56 +01:00
var type = FilterType . Regexp ;
2019-09-29 20:16:28 -07:00
if ( filter is FilterExpression expression )
type = expression . Type ;
2015-05-17 13:24:51 +02:00
// Bugfix: SQLite does not handle case-insensitive LIKE with non-ascii characters
2025-04-03 14:20:00 +02:00
if ( type != FilterType . Regexp && ! Library . Utility . Utility . IsFSCaseSensitive && filter . ToString ()!. Any ( x => x > 127 ))
2025-03-14 14:34:56 +01:00
type = FilterType . Regexp ;
2019-08-05 20:14:05 -04:00
2024-06-21 07:52:07 +02:00
if ( filter . Empty )
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" AS SELECT DISTINCT ""Path"" FROM ""File"" " )). Await ();
return ;
2024-06-21 07:52:07 +02:00
}
2025-03-14 14:34:56 +01:00
if ( type == FilterType . Regexp || type == FilterType . Group )
2013-05-11 12:03:15 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
// TODO: Optimize this to not rely on the "File" view, and not instantiate the paths in full
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" (""Path"" TEXT NOT NULL)" )). Await ();
cmd . SetCommandAndParameters ( FormatInvariant ( $@"INSERT INTO ""{Tablename}"" (""Path"") VALUES (@Path)" ));
using ( var c2 = m_connection . CreateCommand ())
using ( var rd = c2 . ExecuteReaderAsync ( @"SELECT DISTINCT ""Path"" FROM ""File"" " ). Await ())
while ( rd . ReadAsync (). Await ())
2013-05-11 12:03:15 +02:00
{
2025-05-12 16:58:23 +02:00
var p = rd . ConvertValueToString ( 0 );
if ( FilterExpression . Matches ( filter , p ))
{
cmd . SetParameterValue ( "@Path" , p );
cmd . ExecuteNonQueryAsync (). Await ();
}
2013-05-11 12:03:15 +02:00
}
2025-05-12 16:58:23 +02:00
transaction . CommitAsync (). Await ();
2013-05-11 12:03:15 +02:00
}
else
{
var sb = new StringBuilder ();
2025-04-03 14:20:00 +02:00
var args = new Dictionary < string , object? >();
2025-03-14 14:34:56 +01:00
foreach ( var f in (( FilterExpression ) filter ). GetSimpleList ())
2013-05-11 12:03:15 +02:00
{
2025-03-18 22:41:11 +01:00
if ( sb . Length != 0 )
sb . Append ( " OR " );
var argName = $"@Arg{args.Count}" ;
2020-09-01 21:45:27 -07:00
if ( type == FilterType . Wildcard )
2013-05-11 12:03:15 +02:00
{
2025-03-18 22:41:11 +01:00
sb . Append ( FormatInvariant ( @ $"""Path"" LIKE {argName}" ));
args . Add ( argName , f . Replace ( '*' , '%' ). Replace ( '?' , '_' ));
2013-05-11 12:03:15 +02:00
}
else
{
2025-03-18 22:41:11 +01:00
sb . Append ( FormatInvariant ( @ $"""Path"" = {argName}" ));
args . Add ( argName , f );
2013-05-11 12:03:15 +02:00
}
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" (""Path"" TEXT NOT NULL)" )). Await ();
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"INSERT INTO ""{Tablename}"" SELECT DISTINCT ""Path"" FROM ""File"" WHERE {sb}" ), args ). Await ();
transaction . CommitAsync (). Await ();
2013-05-11 12:03:15 +02:00
}
}
2019-08-05 20:14:05 -04:00
2013-05-11 12:03:15 +02:00
public void Dispose ()
{
if ( Tablename != null )
2019-08-05 20:14:05 -04:00
try
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . ExecuteNonQueryAsync ( FormatInvariant ( @ $"DROP TABLE IF EXISTS ""{Tablename}"" " )). Await ();
2013-05-11 12:03:15 +02:00
}
2013-08-23 23:36:25 +02:00
catch { }
2025-04-03 14:20:00 +02:00
finally { Tablename = null !; }
2019-08-05 20:14:05 -04:00
}
2013-05-11 12:03:15 +02:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task RenameRemoteFile ( string oldname , string newname , SqliteTransaction transaction )
2013-07-23 18:56:01 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
//Rename the old entry, to preserve ID links
cmd . SetCommandAndParameters ( @"UPDATE ""Remotevolume"" SET ""Name"" = @Newname WHERE ""Name"" = @Oldname" );
cmd . SetParameterValue ( "@Newname" , newname );
cmd . SetParameterValue ( "@Oldname" , oldname );
var c = await cmd . ExecuteNonQueryAsync ();
2025-03-18 22:41:11 +01:00
2025-05-12 16:58:23 +02:00
if ( c != 1 )
throw new Exception ( $"Unexpected result from renaming \" { oldname } \ " to \"{newname}\", expected {1} got {c}" );
// Grab the type of entry
cmd . SetCommandAndParameters ( @"SELECT ""Type"" FROM ""Remotevolume"" WHERE ""Name"" = @Name" );
cmd . SetParameterValue ( "@Name" , newname );
var type = ( RemoteVolumeType ) Enum . Parse (
typeof ( RemoteVolumeType ), ( await cmd . ExecuteScalarAsync ())?. ToString () ?? "" ,
true );
//Create a fake new entry with the old name and mark as deleting
// as this ensures we will remove it, if it shows up in some later listing
await RegisterRemoteVolume ( oldname , type , RemoteVolumeState . Deleting , transaction );
await transaction . CommitAsync ();
2013-07-23 18:56:01 +02:00
}
2019-08-05 20:14:05 -04:00
2013-07-22 16:54:19 +02:00
/// <summary>
/// Creates a timestamped backup operation to correctly associate the fileset with the time it was created.
/// </summary>
/// <param name="volumeid">The ID of the fileset volume to update</param>
/// <param name="timestamp">The timestamp of the operation to create</param>
/// <param name="transaction">An optional external transaction</param>
2025-05-12 16:58:23 +02:00
public virtual async Task < long > CreateFileset ( long volumeid , DateTime timestamp , SqliteTransaction transaction )
2013-07-22 16:54:19 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"INSERT INTO ""Fileset"" (""OperationID"", ""Timestamp"", ""VolumeID"", ""IsFullBackup"") VALUES (@OperationId, @Timestamp, @VolumeId, @IsFullBackup); SELECT last_insert_rowid();" );
cmd . SetParameterValue ( "@OperationId" , m_operationid );
cmd . SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( timestamp ));
cmd . SetParameterValue ( "@VolumeId" , volumeid );
cmd . SetParameterValue ( "@IsFullBackup" , BackupType . PARTIAL_BACKUP );
var id = await cmd . ExecuteScalarInt64Async (- 1 );
await transaction . CommitAsync ();
return id ;
2013-07-22 16:54:19 +02:00
}
2024-06-21 07:52:07 +02:00
2025-05-08 10:31:38 +02:00
/// <summary>
/// Adds a link between an index volume and a block volume.
/// </summary>
/// <param name="indexVolumeID">The ID of the index volume.</param>
/// <param name="blockVolumeID">The ID of the block volume.</param>
/// <param name="transaction">An optional transaction.</param>
2025-05-12 17:09:35 +02:00
public async Task AddIndexBlockLink ( long indexVolumeID , long blockVolumeID , SqliteTransaction transaction )
2013-07-22 16:54:19 +02:00
{
2025-05-08 10:31:38 +02:00
if ( indexVolumeID <= 0 )
throw new ArgumentOutOfRangeException ( nameof ( indexVolumeID ), "Index volume ID must be greater than 0." );
if ( blockVolumeID <= 0 )
throw new ArgumentOutOfRangeException ( nameof ( blockVolumeID ), "Block volume ID must be greater than 0." );
2025-05-12 17:09:35 +02:00
m_insertIndexBlockLink . Transaction = transaction ;
m_insertIndexBlockLink . SetParameterValue ( "@IndexVolumeId" , indexVolumeID );
m_insertIndexBlockLink . SetParameterValue ( "@BlockVolumeId" , blockVolumeID );
await m_insertIndexBlockLink . ExecuteNonQueryAsync ();
2013-07-22 16:54:19 +02:00
}
2014-12-30 18:26:08 +01:00
2024-09-09 15:27:37 +02:00
/// <summary>
/// Returns all unique blocklists for a given volume
/// </summary>
/// <param name="volumeid">The volume ID to get blocklists for</param>
/// <param name="blocksize">The blocksize</param>
/// <param name="hashsize">The size of the hash</param>
/// <param name="transaction">An optional external transaction</param>
/// <returns>An enumerable of tuples containing the blocklist hash, the blocklist data and the length of the data</returns>
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < Tuple < string , byte [], int >> GetBlocklists ( long volumeid , long blocksize , int hashsize , SqliteTransaction transaction )
2014-12-30 18:26:08 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
// Group subquery by hash to ensure that each blocklist hash appears only once in the result
var sql = FormatInvariant ( $@"SELECT ""A"".""Hash"", ""C"".""Hash"" FROM
2025-03-18 22:41:11 +01:00
(SELECT ""BlocklistHash"".""BlocksetID"", ""Block"".""Hash"", ""BlocklistHash"".""Index"" FROM ""BlocklistHash"",""Block"" WHERE ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""Block"".""VolumeID"" = @VolumeId GROUP BY ""Block"".""Hash"", ""Block"".""Size"") A,
2025-05-12 16:58:23 +02:00
""BlocksetEntry"" B, ""Block"" C WHERE ""B"".""BlocksetID"" = ""A"".""BlocksetID"" AND
""B"".""Index"" >= (""A"".""Index"" * {blocksize / hashsize}) AND ""B"".""Index"" < ((""A"".""Index"" + 1) * {blocksize / hashsize}) AND ""C"".""ID"" = ""B"".""BlockID""
2025-03-14 12:13:17 +01:00
ORDER BY ""A"".""BlocksetID"", ""B"".""Index""" );
2014-12-30 18:26:08 +01:00
2025-05-12 16:58:23 +02:00
string? curHash = null ;
var count = 0 ;
var buffer = new byte [ blocksize ];
2014-12-30 18:26:08 +01:00
2025-05-12 16:58:23 +02:00
cmd . SetCommandAndParameters ( sql );
cmd . SetParameterValue ( "@VolumeId" , volumeid );
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
{
var blockhash = rd . ConvertValueToString ( 0 );
if (( blockhash != curHash && curHash != null ) || count + hashsize > buffer . Length )
2014-12-30 18:26:08 +01:00
{
2025-05-12 16:58:23 +02:00
yield return new Tuple < string , byte [], int >( curHash !, buffer , count );
buffer = new byte [ blocksize ];
count = 0 ;
2014-12-30 18:26:08 +01:00
}
2025-05-12 16:58:23 +02:00
var hash = Convert . FromBase64String ( rd . ConvertValueToString ( 1 ) ?? throw new Exception ( "Hash is null" ));
Array . Copy ( hash , 0 , buffer , count , hashsize );
curHash = blockhash ;
count += hashsize ;
}
if ( curHash != null )
yield return new Tuple < string , byte [], int >( curHash , buffer , count );
2014-12-30 18:26:08 +01:00
}
2015-02-15 23:26:52 +01:00
2019-09-01 13:12:03 -04:00
/// <summary>
2019-09-10 12:53:37 -04:00
/// Update fileset with full backup state
2019-09-01 13:12:03 -04:00
/// </summary>
/// <param name="fileSetId">Existing file set to update</param>
2019-09-10 12:53:37 -04:00
/// <param name="isFullBackup">Full backup state</param>
2019-09-01 13:12:03 -04:00
/// <param name="transaction">An optional external transaction</param>
2025-05-12 16:58:23 +02:00
public async Task UpdateFullBackupStateInFileset ( long fileSetId , bool isFullBackup , SqliteTransaction transaction )
2019-09-01 13:12:03 -04:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"UPDATE ""Fileset"" SET ""IsFullBackup"" = @IsFullBackup WHERE ""ID"" = @FilesetId;" );
cmd . SetParameterValue ( "@FilesetId" , fileSetId );
cmd . SetParameterValue ( "@IsFullBackup" , isFullBackup ? BackupType . FULL_BACKUP : BackupType . PARTIAL_BACKUP );
await cmd . ExecuteNonQueryAsync ();
await transaction . CommitAsync ();
2019-09-01 13:12:03 -04:00
}
2025-04-03 22:20:42 +02:00
/// <summary>
/// Removes all entries in the fileset entry table for a given fileset ID
/// </summary>
/// <param name="filesetId">The fileset ID to clear</param>
/// <param name="transaction">The transaction to use</param>
2025-05-12 16:58:23 +02:00
public async Task ClearFilesetEntries ( long filesetId , SqliteTransaction transaction )
2025-04-03 22:20:42 +02:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" = @FilesetId" );
cmd . SetParameterValue ( "@FilesetId" , filesetId );
await cmd . ExecuteNonQueryAsync ();
2025-04-03 22:20:42 +02:00
}
2025-03-08 15:13:03 +01:00
/// <summary>
/// Gets the last previous fileset that was incomplete
/// </summary>
/// <param name="transaction">The transaction to use</param>
/// <returns>The last incomplete fileset or default</returns>
2025-05-12 16:58:23 +02:00
public async Task < RemoteVolumeEntry > GetLastIncompleteFilesetVolume ( SqliteTransaction transaction )
2025-03-08 15:13:03 +01:00
{
2025-05-12 16:58:23 +02:00
var candidates = GetIncompleteFilesets ( transaction ). OrderBy ( x => x . Value );
if ( await candidates . AnyAsync ())
return await GetRemoteVolumeFromFilesetID (( await candidates . LastAsync ()). Key , transaction );
2025-03-08 15:13:03 +01:00
return default ;
}
/// <summary>
/// Gets a list of incomplete filesets
/// </summary>
/// <param name="transaction">An optional transaction</param>
/// <returns>A list of fileset IDs and timestamps</returns>
2025-05-12 16:58:23 +02:00
public async IAsyncEnumerable < KeyValuePair < long , DateTime >> GetIncompleteFilesets ( SqliteTransaction transaction )
2025-03-08 15:13:03 +01:00
{
2025-05-12 16:58:23 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
using var rd = await cmd . ExecuteReaderAsync ( FormatInvariant ( @ $"SELECT DISTINCT ""Fileset"".""ID"", ""Fileset"".""Timestamp"" FROM ""Fileset"", ""RemoteVolume"" WHERE ""RemoteVolume"".""ID"" = ""Fileset"".""VolumeID"" AND ""Fileset"".""ID"" IN (SELECT ""FilesetID"" FROM ""FilesetEntry"") AND (""RemoteVolume"".""State"" = '{RemoteVolumeState.Uploading}' OR ""RemoteVolume"".""State"" = '{RemoteVolumeState.Temporary}')" ));
while ( await rd . ReadAsync ())
{
yield return new KeyValuePair < long , DateTime >(
rd . ConvertValueToInt64 ( 0 ),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime ()
);
}
2025-03-08 15:13:03 +01:00
}
2019-09-01 13:12:03 -04:00
2025-03-08 15:13:03 +01:00
/// <summary>
/// Gets the remote volume entry from the fileset ID
/// </summary>
/// <param name="filesetID">The fileset ID</param>
/// <param name="transaction">An optional transaction</param>
/// <returns>The remote volume entry or default</returns>
2025-05-12 16:58:23 +02:00
public async Task < RemoteVolumeEntry > GetRemoteVolumeFromFilesetID ( long filesetID , SqliteTransaction transaction )
{
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"SELECT ""RemoteVolume"".""ID"", ""Name"", ""Type"", ""Size"", ""Hash"", ""State"", ""DeleteGraceTime"", ""ArchiveTime"" FROM ""RemoteVolume"", ""Fileset"" WHERE ""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""Fileset"".""ID"" = @FilesetId" );
cmd . SetParameterValue ( "@FilesetId" , filesetID );
using var rd = await cmd . ExecuteReaderAsync ();
if ( await rd . ReadAsync ())
return new RemoteVolumeEntry (
rd . ConvertValueToInt64 ( 0 , - 1 ),
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 4 ),
rd . ConvertValueToInt64 ( 3 , - 1 ),
( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), rd . ConvertValueToString ( 2 ) ?? "" ),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . ConvertValueToString ( 5 ) ?? "" ),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 6 )). ToLocalTime (),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 7 )). ToLocalTime ()
);
else
return default ( RemoteVolumeEntry );
2019-09-01 13:12:03 -04:00
}
2025-05-12 16:58:23 +02:00
public async Task PurgeLogData ( DateTime threshold )
2015-02-15 23:26:52 +01:00
{
2025-05-12 16:58:23 +02:00
using var transaction = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
var t = Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( threshold );
cmd . SetCommandAndParameters ( @"DELETE FROM ""LogData"" WHERE ""Timestamp"" < @Timestamp" );
cmd . SetParameterValue ( "@Timestamp" , t );
await cmd . ExecuteNonQueryAsync ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""RemoteOperation"" WHERE ""Timestamp"" < @Timestamp" );
cmd . SetParameterValue ( "@Timestamp" , t );
await cmd . ExecuteNonQueryAsync ();
await transaction . CommitAsync ();
2015-02-15 23:26:52 +01:00
}
2019-08-05 20:14:05 -04:00
2025-05-12 16:58:23 +02:00
public async Task PurgeDeletedVolumes ( DateTime threshold )
2019-09-07 17:16:34 -04:00
{
2025-05-12 16:58:23 +02:00
using var transaction = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
m_removedeletedremotevolumeCommand . SetParameterValue ( "@Now" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( threshold ));
await m_removedeletedremotevolumeCommand . ExecuteNonQueryAsync ();
await transaction . CommitAsync ();
2019-09-07 17:16:34 -04:00
}
2013-03-27 16:06:45 +01:00
public virtual void Dispose ()
{
2013-05-25 16:40:15 +02:00
if ( IsDisposed )
return ;
2016-04-06 20:40:34 +02:00
2025-05-12 17:09:35 +02:00
DisposeAllFields < SqliteCommand >( this , false );
2016-04-06 20:40:34 +02:00
if ( ShouldCloseConnection && m_connection != null )
{
2025-03-14 14:34:56 +01:00
if ( m_connection . State == ConnectionState . Open && ! m_hasExecutedVacuum )
2019-04-22 21:11:27 -07:00
{
2025-05-12 16:58:23 +02:00
using ( var transaction = m_connection . BeginTransaction ())
using ( var command = m_connection . CreateCommand ())
2019-04-22 21:11:27 -07:00
{
2025-05-12 16:58:23 +02:00
command . Transaction = transaction ;
2025-01-28 08:55:28 +01:00
// SQLite recommends that PRAGMA optimize is run just before closing each database connection.
2025-05-12 17:09:35 +02:00
command . ExecuteNonQueryAsync ( "PRAGMA optimize" ). Await ();
2025-01-28 08:55:28 +01:00
try
2019-04-22 21:11:27 -07:00
{
2025-05-12 16:58:23 +02:00
transaction . CommitAsync (). Await ();
2019-04-22 21:11:27 -07:00
}
2025-04-23 16:40:36 +02:00
catch ( Exception ex )
2025-01-28 08:55:28 +01:00
{
Logging . Log . WriteVerboseMessage ( LOGTAG , "FailedToCommitTransaction" , ex , "Failed to commit transaction after pragma optimize, usually caused by the a no-op transaction" );
}
2019-04-22 21:11:27 -07:00
}
2025-05-12 16:58:23 +02:00
m_connection . CloseAsync (). Await ();
2019-04-22 21:11:27 -07:00
}
2016-04-06 20:40:34 +02:00
m_connection . Dispose ();
}
2013-05-25 16:40:15 +02:00
IsDisposed = true ;
2016-04-06 20:40:34 +02:00
}
/// <summary>
/// Disposes all fields of a certain type, in the instance and its bases
/// </summary>
/// <typeparam name="T">The type of fields to find</typeparam>
/// <param name="item">The item to dispose</param>
/// <param name="throwExceptions"><c>True</c> if an aggregate exception should be thrown, or <c>false</c> if exceptions are silently captured</param>
public static void DisposeAllFields < T >( object item , bool throwExceptions )
where T : IDisposable
{
var typechain = new List < Type >();
var cur = item . GetType ();
var exceptions = new List < Exception >();
while ( cur != null && cur != typeof ( object ))
{
typechain . Add ( cur );
cur = cur . BaseType ;
}
var fields =
typechain . SelectMany ( x =>
x . GetFields ( System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Public | System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . FlattenHierarchy )
). Distinct (). Where ( x => x . FieldType . IsAssignableFrom ( typeof ( T )));
foreach ( var p in fields )
try
{
var val = p . GetValue ( item );
if ( val != null )
(( T ) val ). Dispose ();
}
catch ( Exception ex )
{
if ( throwExceptions )
exceptions . Add ( ex );
}
if ( exceptions . Count > 0 )
throw new AggregateException ( exceptions );
2014-07-16 00:57:57 +02:00
}
2025-05-12 16:58:23 +02:00
public async Task WriteResults ( IBasicResults result )
2014-07-16 00:57:57 +02:00
{
if ( IsDisposed )
return ;
2025-03-20 09:51:49 +01:00
if ( m_connection != null && result != null )
2013-05-25 16:40:15 +02:00
{
2025-03-20 09:51:49 +01:00
if ( result is BasicResults basicResults )
{
basicResults . FlushLog ( this );
if ( basicResults . EndTime . Ticks == 0 )
basicResults . EndTime = DateTime . UtcNow ;
}
2016-12-01 23:59:54 +01:00
2018-11-14 08:47:01 -02:00
var serializer = new JsonFormatSerializer ();
2025-05-12 16:58:23 +02:00
await LogMessage ( "Result" ,
2025-03-20 09:51:49 +01:00
serializer . SerializeResults ( result ),
2016-09-28 20:20:16 +02:00
null ,
null
);
2013-05-25 16:40:15 +02:00
}
2013-03-27 16:06:45 +01:00
}
2018-06-14 10:12:24 +02:00
/// <summary>
/// The current index into the path prefix buffer
/// </summary>
private int m_pathPrefixIndex = 0 ;
/// <summary>
/// The path prefix lookup list
/// </summary>
private readonly KeyValuePair < string , long >[] m_pathPrefixLookup = new KeyValuePair < string , long >[ 5 ];
/// <summary>
/// Gets the path prefix ID, optionally creating it in the process.
/// </summary>
/// <returns>The path prefix ID.</returns>
/// <param name="prefix">The path to get the prefix for.</param>
/// <param name="transaction">The transaction to use for insertion, or null for no transaction</param>
2025-05-12 16:58:23 +02:00
public async Task < long > GetOrCreatePathPrefix ( string prefix , SqliteTransaction transaction )
2018-06-14 10:12:24 +02:00
{
// Ring-buffer style lookup
for ( var i = 0 ; i < m_pathPrefixLookup . Length ; i ++)
{
var ix = ( i + m_pathPrefixIndex ) % m_pathPrefixLookup . Length ;
if ( string . Equals ( m_pathPrefixLookup [ ix ]. Key , prefix , StringComparison . Ordinal ))
return m_pathPrefixLookup [ ix ]. Value ;
}
m_findpathprefixCommand . Transaction = transaction ;
2025-05-12 16:58:23 +02:00
m_findpathprefixCommand . SetParameterValue ( "@Prefix" , prefix );
var id = await m_findpathprefixCommand . ExecuteScalarInt64Async ();
2025-03-18 22:41:11 +01:00
2018-06-14 10:12:24 +02:00
if ( id < 0 )
2025-05-12 16:58:23 +02:00
{
m_insertpathprefixCommand . SetParameterValue ( "@Prefix" , prefix );
m_insertpathprefixCommand . Transaction = transaction ;
id = await m_insertpathprefixCommand . ExecuteScalarInt64Async ();
}
2018-06-14 10:12:24 +02:00
m_pathPrefixIndex = ( m_pathPrefixIndex + 1 ) % m_pathPrefixLookup . Length ;
m_pathPrefixLookup [ m_pathPrefixIndex ] = new KeyValuePair < string , long >( prefix , id );
return id ;
}
/// <summary>
/// The path separators on this system
/// </summary>
2025-05-12 16:58:23 +02:00
private static readonly char [] _pathseparators = [
2025-03-14 14:34:56 +01:00
Path . DirectorySeparatorChar ,
Path . AltDirectorySeparatorChar ,
2025-05-12 16:58:23 +02:00
];
2018-06-14 10:12:24 +02:00
/// <summary>
/// Helper method that splits a path on the last path separator
/// </summary>
/// <returns>The prefix and name.</returns>
/// <param name="path">The path to split.</param>
public static KeyValuePair < string , string > SplitIntoPrefixAndName ( string path )
{
2019-02-04 17:32:56 +01:00
if ( string . IsNullOrEmpty ( path ))
2018-06-14 10:12:24 +02:00
throw new ArgumentException ( $"Invalid path: {path}" , nameof ( path ));
int nLast = path . TrimEnd ( _pathseparators ). LastIndexOfAny ( _pathseparators );
if ( nLast >= 0 )
return new KeyValuePair < string , string >( path . Substring ( 0 , nLast + 1 ), path . Substring ( nLast + 1 ));
2019-02-04 17:32:56 +01:00
return new KeyValuePair < string , string >( string . Empty , path );
2019-08-05 20:14:05 -04:00
}
2019-09-15 13:45:43 -07:00
}
2019-08-18 14:41:05 -04:00
2019-09-15 13:45:43 -07:00
/// <summary>
/// Defines the backups types
/// </summary>
public static class BackupType
{
public const int PARTIAL_BACKUP = 0 ;
public const int FULL_BACKUP = 1 ;
2013-03-27 16:06:45 +01:00
}
}