2025-01-14 19:42:03 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
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 ;
2013-03-27 16:06:45 +01:00
using System.Linq ;
using System.Text ;
2013-04-21 20:00:37 +02:00
using System.IO ;
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 ;
2024-05-11 23:37:21 +02:00
using System.Runtime.CompilerServices ;
2025-03-10 14:41:36 +01:00
using Duplicati.Library.Interface ;
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-03-14 14:34:56 +01:00
protected readonly IDbConnection m_connection ;
2013-03-27 16:06:45 +01:00
protected readonly long m_operationid = - 1 ;
2025-01-14 19:42:03 +01:00
private bool m_hasExecutedVacuum ;
2013-03-27 16:06:45 +01:00
2025-03-14 14:34:56 +01:00
private readonly IDbCommand m_updateremotevolumeCommand ;
private readonly IDbCommand m_selectremotevolumesCommand ;
private readonly IDbCommand m_selectremotevolumeCommand ;
private readonly IDbCommand m_removeremotevolumeCommand ;
private readonly IDbCommand m_removedeletedremotevolumeCommand ;
private readonly IDbCommand m_selectremotevolumeIdCommand ;
private readonly IDbCommand m_createremotevolumeCommand ;
private readonly IDbCommand m_selectduplicateRemoteVolumesCommand ;
2013-03-27 16:06:45 +01:00
2025-03-14 14:34:56 +01:00
private readonly IDbCommand m_insertlogCommand ;
private readonly IDbCommand m_insertremotelogCommand ;
private readonly IDbCommand m_insertIndexBlockLink ;
2013-03-27 16:06:45 +01:00
2025-03-14 14:34:56 +01:00
private readonly IDbCommand m_findpathprefixCommand ;
private readonly IDbCommand 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-03-14 14:34:56 +01:00
internal IDbConnection 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-03-14 14:34:56 +01:00
protected static IDbConnection CreateConnection ( string path )
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-03-14 14:34:56 +01:00
var c = SQLiteHelper . SQLiteLoader . LoadConnection ( path );
2013-03-08 22:24:54 +01:00
2020-12-28 12:18:58 -08:00
try
{
2025-03-14 14:34:56 +01:00
SQLiteHelper . DatabaseUpgrader . UpgradeDatabase ( c , path , typeof ( LocalDatabase ));
2020-12-28 12:18:58 -08:00
}
catch
{
//Don't leak database connections when something goes wrong
c . Dispose ();
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>
2016-04-06 20:40:34 +02:00
public LocalDatabase ( string path , string operation , bool shouldclose )
2013-03-08 22:24:54 +01:00
: this ( CreateConnection ( path ), operation )
{
2016-04-06 20:40:34 +02:00
ShouldCloseConnection = shouldclose ;
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 ;
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-03-14 14:34:56 +01:00
public LocalDatabase ( IDbConnection 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 )
{
using ( var cmd = m_connection . CreateCommand ())
2025-03-18 22:41:11 +01:00
m_operationid = cmd . SetCommandAndParameters ( @"INSERT INTO ""Operation"" (""Description"", ""Timestamp"") VALUES (@Description, @Timestamp); SELECT last_insert_rowid();" )
. SetParameterValue ( "@Description" , operation )
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( OperationTimestamp ))
. ExecuteScalarInt64 (- 1 );
2023-09-21 19:58:27 +02:00
}
else
{
// Get last operation
using ( var cmd = m_connection . CreateCommand ())
using ( var rd = cmd . ExecuteReader ( @"SELECT ""ID"", ""Timestamp"" FROM ""Operation"" ORDER BY ""Timestamp"" DESC LIMIT 1" ))
{
2024-06-21 07:52:07 +02:00
if (! rd . Read ())
2023-09-21 19:58:27 +02:00
throw new Exception ( "LocalDatabase does not contain a previous operation." );
2025-03-18 22:41:11 +01:00
2025-04-03 15:46:20 +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-03-14 14:34:56 +01:00
private LocalDatabase ( IDbConnection 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" );
m_createremotevolumeCommand = connection . CreateCommand ( @"INSERT INTO ""Remotevolume"" (""OperationID"", ""Name"", ""Type"", ""State"", ""Size"", ""VerificationCount"", ""DeleteGraceTime"") VALUES (@OperationID, @Name, @Type, @State, @Size, @VerificationCount, @DeleteGraceTime); SELECT last_insert_rowid();" );
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
/// </summary>
public static DateTime ParseFromEpochSeconds ( long seconds )
{
return Library . Utility . Utility . EPOCH . AddSeconds ( seconds );
}
2016-03-16 00:49:28 +01:00
2025-04-23 15:40:16 +02:00
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , IDbTransaction ? transaction = null )
2019-08-05 20:14:05 -04:00
{
UpdateRemoteVolume ( name , state , size , hash , false , transaction );
2016-03-24 16:30:19 +01:00
}
2016-03-16 00:49:28 +01:00
2025-04-23 15:40:16 +02:00
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , bool suppressCleanup , IDbTransaction ? transaction = null )
2016-03-24 16:30:19 +01:00
{
2025-01-29 21:44:06 +01:00
UpdateRemoteVolume ( name , state , size , hash , suppressCleanup , new TimeSpan ( 0 ), null , transaction );
2016-03-24 16:30:19 +01:00
}
2025-04-23 22:37:31 +02:00
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string? hash , bool suppressCleanup , TimeSpan deleteGraceTime , bool? setArchived , IDbTransaction ? transaction = null )
2013-03-27 16:06:45 +01:00
{
2013-04-09 20:43:27 +02:00
m_updateremotevolumeCommand . Transaction = transaction ;
2025-03-18 22:41:11 +01:00
var c = m_updateremotevolumeCommand . SetParameterValue ( "@OperationID" , m_operationid )
. SetParameterValue ( "@State" , state . ToString ())
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
. SetParameterValue ( "@Name" , name )
. ExecuteNonQuery ();
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
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2019-09-07 17:16:34 -04:00
{
2025-03-18 22:41:11 +01:00
c = cmd . SetCommandAndParameters ( @"UPDATE ""RemoteVolume"" SET ""DeleteGraceTime"" = @DeleteGraceTime WHERE ""Name"" = @Name " )
. SetParameterValue ( "@DeleteGraceTime" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow + deleteGraceTime ))
. SetParameterValue ( "@Name" , name )
. ExecuteNonQuery ();
if ( c != 1 )
2019-09-07 17:16:34 -04:00
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
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
{
2025-04-23 22:37:31 +02:00
c = cmd . SetCommandAndParameters ( @"UPDATE ""RemoteVolume"" SET ""ArchiveTime"" = @ArchiveTime WHERE ""Name"" = @Name " )
2025-04-24 20:30:08 +02:00
. SetParameterValue ( "@ArchiveTime" , setArchived . Value ? Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ) : 0 )
2025-04-23 22:37:31 +02:00
. SetParameterValue ( "@Name" , name )
. ExecuteNonQuery ();
if ( c != 1 )
2025-01-29 11:36:45 +01:00
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
{
2019-08-05 20:14:05 -04:00
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
2013-05-11 22:56:21 +02:00
public IEnumerable < KeyValuePair < long , DateTime >> FilesetTimes
2019-08-05 20:14:05 -04:00
{
get
2013-05-11 22:56:21 +02:00
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
2024-06-21 07:52:07 +02:00
using ( var rd = cmd . ExecuteReader ( @"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC" ))
2013-05-11 22:56:21 +02:00
while ( rd . Read ())
2025-04-03 15:46:20 +02:00
yield return new KeyValuePair < long , DateTime >( rd . ConvertValueToInt64 ( 0 ), ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime ());
2013-05-11 22:56:21 +02:00
}
}
2025-04-03 14:20:00 +02:00
public ( 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-03-14 14:34:56 +01:00
var filesets = ( filesetslist ?? FilesetTimes ). ToArray ();
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 );
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-04-03 14:20:00 +02:00
public long GetRemoteVolumeID ( string file , IDbTransaction ? transaction = null )
2016-09-15 11:39:27 +02:00
{
m_selectremotevolumeIdCommand . Transaction = transaction ;
2025-03-18 22:41:11 +01:00
return m_selectremotevolumeIdCommand . SetParameterValue ( "@Name" , file ). ExecuteScalarInt64 (- 1 );
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
2025-04-03 14:20:00 +02:00
public IEnumerable < KeyValuePair < string , long >> GetRemoteVolumeIDs ( IEnumerable < string > files , IDbTransaction ? transaction = null )
2024-11-01 14:50:26 +01:00
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
{
2025-04-02 23:20:58 +02:00
using var tmptable = new TemporaryDbValueList ( m_connection , transaction , files );
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( @"SELECT ""Name"", ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" IN (@Name)" )
2025-04-02 23:20:58 +02:00
. ExpandInClauseParameter ( "@Name" , tmptable );
2024-11-01 14:50:26 +01:00
using ( var rd = cmd . ExecuteReader ())
while ( rd . Read ())
2025-04-03 15:46:20 +02:00
yield return new KeyValuePair < string , long >( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToInt64 ( 1 ));
2024-11-01 14:50:26 +01:00
}
}
2025-04-03 14:20:00 +02:00
public RemoteVolumeEntry GetRemoteVolume ( string file , IDbTransaction ? 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 );
2019-08-05 20:14:05 -04:00
using ( var rd = m_selectremotevolumeCommand . ExecuteReader ())
2013-03-27 16:06:45 +01:00
if ( rd . Read ())
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-04-24 20:28:26 +02:00
public IEnumerable < KeyValuePair < string , RemoteVolumeState >> DuplicateRemoteVolumes ( IDbTransaction ? transaction )
2016-03-24 16:31:07 +01:00
{
2025-04-24 20:28:26 +02:00
foreach ( var rd in m_selectduplicateRemoteVolumesCommand . SetTransaction ( transaction ). ExecuteReaderEnumerable ())
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-04-03 14:20:00 +02:00
public IEnumerable < RemoteVolumeEntry > GetRemoteVolumes ( IDbTransaction ? transaction = null )
2013-03-27 16:06:45 +01:00
{
2016-02-22 21:27:12 +01:00
m_selectremotevolumesCommand . Transaction = transaction ;
2013-03-27 16:06:45 +01:00
using ( var rd = m_selectremotevolumesCommand . ExecuteReader ())
{
while ( rd . Read ())
{
2013-07-01 11:58:33 +02:00
yield return new RemoteVolumeEntry (
2016-02-22 21:27:12 +01:00
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 ))
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-04-23 15:40:16 +02:00
public void LogRemoteOperation ( string operation , string path , string? data , IDbTransaction ? transaction )
2013-03-27 16:06:45 +01:00
{
2025-03-18 22:41:11 +01:00
m_insertremotelogCommand
. SetParameterValue ( "@OperationID" , m_operationid )
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ))
. SetParameterValue ( "@Operation" , operation )
. SetParameterValue ( "@Path" , path )
. SetParameterValue ( "@Data" , data )
. ExecuteNonQuery ( transaction );
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-04-03 14:20:00 +02:00
public void LogMessage ( string type , string message , Exception ? exception , IDbTransaction ? transaction )
2013-03-27 16:06:45 +01:00
{
2025-03-18 22:41:11 +01:00
m_insertlogCommand . SetParameterValue ( "@OperationID" , m_operationid )
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ))
. SetParameterValue ( "@Type" , type )
. SetParameterValue ( "@Message" , message )
. SetParameterValue ( "@Exception" , exception ?. ToString ())
. ExecuteNonQuery ( transaction );
2013-03-27 16:06:45 +01:00
}
2025-04-03 14:20:00 +02:00
public void UnlinkRemoteVolume ( string name , RemoteVolumeState state , IDbTransaction ? transaction = null )
2016-03-24 16:31:07 +01:00
{
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2016-03-24 16:31:07 +01:00
{
2025-03-18 22:41:11 +01:00
var c = cmd . SetCommandAndParameters ( @"DELETE FROM ""RemoteVolume"" WHERE ""Name"" = @Name AND ""State"" = @State " )
. SetParameterValue ( "@Name" , name )
. SetParameterValue ( "@State" , state . ToString ())
. ExecuteNonQuery ();
2016-03-24 16:31:07 +01:00
if ( c != 1 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Unexpected number of remote volumes deleted: {c}, expected {1}" );
2016-03-24 16:31:07 +01:00
tr . Commit ();
}
}
2025-04-03 14:20:00 +02:00
public void RemoveRemoteVolume ( string name , IDbTransaction ? transaction = null )
2013-03-27 16:06:45 +01:00
{
2025-03-08 15:13:03 +01:00
RemoveRemoteVolumes ([ name ], transaction );
2016-03-16 00:49:28 +01:00
}
2025-04-03 14:20:00 +02:00
public void RemoveRemoteVolumes ( IEnumerable < string > names , IDbTransaction ? transaction = null )
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
2013-04-09 20:43:27 +02:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var deletecmd = m_connection . CreateCommand ( tr . Parent ))
2013-03-27 16:06:45 +01:00
{
2016-03-16 00:49:28 +01:00
string temptransguid = Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
var volidstable = "DelVolSetIds-" + temptransguid ;
var blocksetidstable = "DelBlockSetIds-" + temptransguid ;
// Create and fill a temp table with the volids to delete. We avoid using too many parameters that way.
2025-03-14 12:13:17 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMP TABLE ""{volidstable}"" (""ID"" INTEGER PRIMARY KEY)" ));
2025-03-18 22:41:11 +01:00
deletecmd . SetCommandAndParameters ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{volidstable}"" (""ID"") VALUES (@Id)" ));
2016-03-16 00:49:28 +01:00
foreach ( var name in names )
{
var volumeid = GetRemoteVolumeID ( name , tr . Parent );
2025-03-18 22:41:11 +01:00
deletecmd . SetParameterValue ( "@Id" , volumeid )
. ExecuteNonQuery ();
2016-03-16 00:49:28 +01:00
}
2025-03-14 12:13:17 +01:00
var volIdsSubQuery = FormatInvariant ( $@"SELECT ""ID"" FROM ""{volidstable}"" " );
2016-03-16 00:49:28 +01:00
deletecmd . Parameters . Clear ();
2019-08-05 20:14:05 -04:00
2025-03-14 12:13:17 +01:00
var bsIdsSubQuery = FormatInvariant ( @ $"
SELECT DISTINCT ""BlocksetEntry"".""BlocksetID"" FROM ""BlocksetEntry"", ""Block""
WHERE ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""Block"".""VolumeID"" IN ({volIdsSubQuery})
UNION ALL
SELECT DISTINCT ""BlocksetID"" FROM ""BlocklistHash""
WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"" WHERE ""VolumeID"" IN ({volIdsSubQuery}))" );
2013-04-08 22:24:54 +02:00
2016-03-13 13:23:06 +01:00
// Create a temporary table to cache subquery result, as it might take long (SQLite does not cache at all).
2025-03-14 12:13:17 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMP TABLE ""{blocksetidstable}"" (""ID"" INTEGER PRIMARY KEY)" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{blocksetidstable}"" (""ID"") {bsIdsSubQuery}" ));
bsIdsSubQuery = FormatInvariant ( $@"SELECT DISTINCT ""ID"" FROM ""{blocksetidstable}"" " );
2016-03-13 13:23:06 +01:00
deletecmd . Parameters . Clear ();
2020-02-23 13:23:21 -06:00
// Create a temp table to associate metadata that is being deleted to a fileset
2025-03-13 23:06:32 +01:00
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
var metadataFilesetTable = @"DelMetadataFilesetIds-" + temptransguid ;
2025-03-13 23:06:32 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMP TABLE ""{metadataFilesetTable}"" (MetadataID INTEGER PRIMARY KEY, FilesetID INTEGER)" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"INSERT OR IGNORE INTO ""{metadataFilesetTable}"" (MetadataID, FilesetID) {metadataFilesetQuery}" ));
2020-02-23 13:23:21 -06:00
// Delete FilesetEntry rows that had their metadata deleted
2025-03-13 23:06:32 +01:00
deletecmd . ExecuteNonQuery ( 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
// Delete FilesetEntry rows that had their blocks deleted
2025-03-13 23:06:32 +01:00
deletecmd . ExecuteNonQuery ( 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}))" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM FileLookup WHERE FileLookup.MetadataID IN (SELECT MetadataID FROM ""{metadataFilesetTable}"")" ));
2020-02-23 13:23:21 -06:00
2025-03-14 12:13:17 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""FileLookup"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""Blockset"" WHERE ""ID"" IN ({bsIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""BlocksetEntry"" WHERE ""BlocksetID"" IN ({bsIdsSubQuery})" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""BlocklistHash"" WHERE ""BlocklistHash"".""BlocksetID"" IN ({bsIdsSubQuery})" ));
2020-02-23 13:23:21 -06:00
// If the volume is a block or index volume, this will update the crosslink table, otherwise nothing will happen
2025-03-14 12:13:17 +01:00
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}))" ));
2025-03-13 23:06:32 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM FilesetEntry WHERE FilesetID IN (SELECT ID FROM Fileset WHERE VolumeID IN ({volIdsSubQuery}))" ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM Fileset WHERE VolumeID IN ({volIdsSubQuery})" ));
2020-02-23 13:23:21 -06:00
// Delete from Fileset if FilesetEntry rows were deleted by related metadata and there are no references in FilesetEntry anymore
2025-03-13 23:06:32 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM Fileset WHERE Fileset.ID IN
2020-02-23 13:23:21 -06:00
(SELECT DISTINCT FilesetID FROM ""{metadataFilesetTable}"")
AND Fileset.ID NOT IN
2025-03-13 23:06:32 +01:00
(SELECT DISTINCT FilesetID FROM FilesetEntry)" ));
2013-03-27 16:06:45 +01:00
2016-03-16 00:49:28 +01:00
// Clean up temp tables for subqueries. We truncate content and then try to delete.
2016-03-13 13:23:06 +01:00
// Drop in try-block, as it fails in nested transactions (SQLite problem)
2025-03-14 14:34:56 +01:00
// SQLite.SQLiteException (0x80004005): database table is locked
2025-03-14 12:13:17 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{blocksetidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DELETE FROM ""{volidstable}"" " ));
2016-03-13 13:23:06 +01:00
try
{
deletecmd . CommandTimeout = 2 ;
2025-03-14 12:13:17 +01:00
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{blocksetidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{volidstable}"" " ));
deletecmd . ExecuteNonQuery ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{metadataFilesetTable}"" " ));
2016-03-13 13:23:06 +01:00
}
2018-12-03 08:13:58 -02:00
catch { /* Ignore, will be deleted on close anyway. */ }
2019-08-05 20:14:05 -04:00
m_removeremotevolumeCommand . Transaction = tr . Parent ;
2025-03-18 22:41:11 +01:00
m_removeremotevolumeCommand . SetParameterValue ( "@Now" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
m_removeremotevolumeCommand . SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ());
2019-08-05 20:14:05 -04:00
foreach ( var name in names )
2016-03-16 00:49:28 +01:00
{
2025-03-18 22:41:11 +01:00
m_removeremotevolumeCommand . SetParameterValue ( "@Name" , name );
2016-03-16 00:49:28 +01:00
m_removeremotevolumeCommand . ExecuteNonQuery ();
}
2019-09-07 17:16:34 -04:00
2025-03-20 15:59:14 +01:00
// Validate before commiting changes
var nonAttachedFiles = deletecmd . ExecuteScalarInt64 ( @"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" );
2013-06-10 23:04:44 +02:00
tr . Commit ();
2013-03-27 16:06:45 +01:00
}
}
2019-08-05 20:14:05 -04:00
2013-08-23 22:15:07 +02:00
public void Vacuum ()
{
2025-01-14 19:42:03 +01:00
m_hasExecutedVacuum = true ;
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
2013-08-23 22:15:07 +02:00
cmd . ExecuteNonQuery ( "VACUUM" );
}
2013-03-27 16:06:45 +01:00
2015-08-24 10:50:47 +01:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , long size , RemoteVolumeState state )
2015-04-05 14:33:13 +02:00
{
2015-08-24 10:50:47 +01:00
return RegisterRemoteVolume ( name , type , state , size , new TimeSpan ( 0 ), null );
2015-04-05 14:33:13 +02:00
}
2025-04-03 14:20:00 +02:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , IDbTransaction ? transaction )
2015-04-05 14:33:13 +02:00
{
return RegisterRemoteVolume ( name , type , state , new TimeSpan ( 0 ), transaction );
}
2015-08-24 10:50:47 +01:00
2025-04-03 14:20:00 +02:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , TimeSpan deleteGraceTime , IDbTransaction ? transaction )
2015-08-24 10:50:47 +01:00
{
return RegisterRemoteVolume ( name , type , state , - 1 , deleteGraceTime , transaction );
}
2019-08-05 20:14:05 -04:00
2025-04-03 14:20:00 +02:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , long size , TimeSpan deleteGraceTime , IDbTransaction ? transaction )
2016-09-15 11:39:27 +02:00
{
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2016-09-15 11:39:27 +02:00
{
2025-03-18 22:41:11 +01:00
var r = m_createremotevolumeCommand . SetParameterValue ( "@OperationId" , m_operationid )
. SetParameterValue ( "@Name" , name )
. SetParameterValue ( "@Type" , type . ToString ())
. SetParameterValue ( "@State" , state . ToString ())
. SetParameterValue ( "@Size" , size )
. SetParameterValue ( "@VerificationCount" , 0 )
. SetParameterValue ( "@DeleteGraceTime" , deleteGraceTime . Ticks <= 0 ? 0 : ( DateTime . UtcNow + deleteGraceTime ). Ticks )
. ExecuteScalarInt64 ( tr . Parent );
2019-08-05 20:14:05 -04:00
2013-06-10 23:04:44 +02:00
tr . Commit ();
2013-04-09 20:43:27 +02:00
return r ;
2013-03-27 16:06:45 +01:00
}
}
2013-08-24 22:27:30 +02:00
public IEnumerable < long > GetFilesetIDs ( DateTime restoretime , long [] versions )
{
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-04-03 14:20:00 +02:00
( var query , var values ) = GetFilelistWhereClause ( restoretime , versions );
2013-08-24 22:27:30 +02:00
var res = new List < long >();
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
{
2025-04-03 14:20:00 +02:00
using ( var rd = cmd . ExecuteReader ( $@"SELECT ""ID"" FROM ""Fileset"" {query} ORDER BY ""Timestamp"" DESC" , values ))
2013-08-24 22:27:30 +02:00
while ( rd . Read ())
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 )
2013-03-27 16:06:45 +01:00
{
cmd . Parameters . Clear ();
2019-08-05 20:14:05 -04:00
using ( var rd = cmd . ExecuteReader ( @"SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC " ))
while ( rd . Read ())
res . Add ( rd . ConvertValueToInt64 ( 0 ));
2013-08-24 22:27:30 +02:00
if ( res . Count == 0 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( "No backup at the specified date" , "NoBackupAtDate" );
2013-08-06 22:57:03 +02:00
else
2018-03-12 14:07:11 +01:00
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
}
2013-08-24 22:27:30 +02:00
return res ;
2013-03-27 16:06:45 +01:00
}
2013-03-08 22:24:54 +01:00
}
2015-04-08 21:01:36 +02:00
public IEnumerable < long > FindMatchingFilesets ( DateTime restoretime , long [] versions )
{
if ( restoretime . Kind == DateTimeKind . Unspecified )
throw new Exception ( "Invalid DateTime given, must be either local or UTC" );
var tmp = GetFilelistWhereClause ( restoretime , versions , singleTimeMatch : true );
string query = tmp . Item1 ;
var args = tmp . Item2 ;
var res = new List < long >();
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
using ( var rd = cmd . ExecuteReader ( @"SELECT ""ID"" FROM ""Fileset"" " + query + @" ORDER BY ""Timestamp"" DESC" , args ))
2015-04-08 21:01:36 +02:00
while ( rd . Read ())
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
public bool IsFilesetFullBackup ( DateTime filesetTime )
{
using ( var cmd = m_connection . CreateCommand ())
2025-03-18 22:41:11 +01:00
using ( var rd = cmd . SetCommandAndParameters ( $@"SELECT ""IsFullBackup"" FROM ""Fileset"" WHERE ""Timestamp"" = @Timestamp" ). SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( filesetTime )). ExecuteReader ())
2019-08-05 20:14:05 -04:00
{
2025-03-14 14:34:56 +01:00
if (! rd . Read ())
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
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
using ( var rd = cmd . ExecuteReader ( @"SELECT ""Key"", ""Value"" FROM ""Configuration"" " ))
while ( rd . Read ())
2025-04-03 14:20:00 +02:00
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-04-03 14:20:00 +02:00
public IDictionary < string , string > GetDbOptions ( IDbTransaction ? transaction = null )
2016-09-15 11:39:27 +02:00
{
2019-08-05 20:14:05 -04:00
return GetDbOptionList ( transaction ). ToDictionary ( x => x . Key , x => x . Value );
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-03-06 12:02:11 +01:00
private void UpdateDbOption ( string key , bool value )
2016-03-18 13:26:12 +01:00
{
2025-03-06 12:02:11 +01:00
var opts = GetDbOptions ();
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-03-06 12:02:11 +01:00
SetDbOptions ( opts );
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-03-06 12:02:11 +01:00
get => GetDbOptions (). ContainsKey ( "repair-in-progress" );
set => UpdateDbOption ( "repair-in-progress" , value );
}
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
{
get => GetDbOptions (). ContainsKey ( "partially-recreated" );
set => UpdateDbOption ( "partially-recreated" , value );
}
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-03-06 14:56:35 +01:00
get => GetDbOptions (). ContainsKey ( "terminated-with-active-uploads" );
set => UpdateDbOption ( "terminated-with-active-uploads" , value );
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-04-03 14:20:00 +02:00
public void SetDbOptions ( IDictionary < string , string > options , IDbTransaction ? transaction = null )
2016-09-15 11:39:27 +02:00
{
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2016-09-15 11:39:27 +02:00
{
cmd . ExecuteNonQuery ( @"DELETE FROM ""Configuration"" " );
2019-08-05 20:14:05 -04:00
foreach ( var kp in options )
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( @"INSERT INTO ""Configuration"" (""Key"", ""Value"") VALUES (@Key, @Value) " )
. SetParameterValue ( "@Key" , kp . Key )
. SetParameterValue ( "@Value" , kp . Value )
. ExecuteNonQuery ();
2019-08-05 20:14:05 -04:00
2016-09-15 11:39:27 +02:00
tr . Commit ();
}
}
public long GetBlocksLargerThan ( long fhblocksize )
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
2025-03-18 22:41:11 +01:00
return cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""Block"" WHERE ""Size"" > @Size" )
. SetParameterValue ( "@Size" , fhblocksize )
. ExecuteScalarInt64 (- 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-03-14 14:34:56 +01:00
public void VerifyConsistency ( long blocksize , long hashsize , bool verifyfilelists , IDbTransaction transaction )
2025-03-25 11:56:52 +01:00
=> VerifyConsistencyInner ( blocksize , hashsize , verifyfilelists , false , transaction );
/// <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>
public void VerifyConsistencyForRepair ( long blocksize , long hashsize , bool verifyfilelists , IDbTransaction transaction )
=> VerifyConsistencyInner ( blocksize , hashsize , verifyfilelists , true , transaction );
/// <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>
private void VerifyConsistencyInner ( long blocksize , long hashsize , bool verifyfilelists , bool laxVerifyForRepair , IDbTransaction transaction )
2013-04-21 20:00:37 +02:00
{
2016-04-04 22:47:16 +02:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2013-04-21 20:00:37 +02:00
{
2013-09-25 23:31:48 +02:00
// Calculate the lengths for each blockset
2016-04-04 22:47:16 +02:00
var combinedLengths = @"
SELECT
""A"".""ID"" AS ""BlocksetID"",
IFNULL(""B"".""CalcLen"", 0) AS ""CalcLen"",
""A"".""Length""
FROM
""Blockset"" A
LEFT OUTER JOIN
(
SELECT
""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""
" ;
2013-09-25 23:31:48 +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
using ( var rd = cmd . ExecuteReader ( reportDetails ))
2016-09-15 11:39:27 +02:00
if ( rd . Read ())
{
var sb = new StringBuilder ();
sb . AppendLine ( "Found inconsistency in the following files while validating database: " );
var c = 0 ;
do
{
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 ++;
2019-08-05 20:14:05 -04:00
} while ( rd . Read ());
2016-09-15 11:39:27 +02:00
c -= 5 ;
if ( c > 0 )
sb . AppendFormat ( "... and {0} more" , c );
2019-08-05 20:14:05 -04:00
2015-01-12 23:07:57 +01:00
sb . Append ( ". Run repair to fix it." );
2025-03-10 14:37:30 +01:00
throw new DatabaseInconsistencyException ( sb . ToString ());
2016-09-15 11:39:27 +02:00
}
2014-12-30 16:13:31 +01:00
2015-01-24 21:59:53 +01:00
var real_count = cmd . ExecuteScalarInt64 ( @"SELECT Count(*) FROM ""BlocklistHash""" , 0 );
var unique_count = cmd . ExecuteScalarInt64 ( @"SELECT Count(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")" , 0 );
2014-12-30 16:13:31 +01:00
2015-01-24 21:59:53 +01:00
if ( real_count != unique_count )
2025-03-13 23:06:32 +01:00
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-03-14 12:13:17 +01:00
var itemswithnoblocklisthash = cmd . ExecuteScalarInt64 ( 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 );
2015-02-03 23:47:33 +01:00
if ( itemswithnoblocklisthash != 0 )
2025-03-13 23:06:32 +01:00
throw new DatabaseInconsistencyException ( $"Found {itemswithnoblocklisthash} file(s) with missing blocklist hashes" );
2015-02-03 23:47:33 +01:00
2017-08-12 19:14:11 +01:00
if ( cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM ""Blockset"" WHERE ""Length"" > 0 AND ""ID"" NOT IN (SELECT ""BlocksetId"" FROM ""BlocksetEntry"")" ) != 0 )
2025-03-10 14:37:30 +01:00
throw new DatabaseInconsistencyException ( "Detected non-empty blocksets with no associated blocks!" );
2016-03-30 12:29:22 +02:00
2025-03-18 22:41:11 +01:00
if ( cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""FileLookup"" WHERE ""BlocksetID"" != @FolderBlocksetId AND ""BlocksetID"" != @SymlinkBlocksetId AND NOT ""BlocksetID"" IN (SELECT ""ID"" FROM ""Blockset"")" )
. SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID )
. SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID )
. ExecuteScalarInt64 ( 0 ) != 0 )
2025-03-10 14:37:30 +01:00
throw new DatabaseInconsistencyException ( "Detected files associated with non-existing blocksets!" );
2016-04-04 18:11:48 +02:00
2025-03-25 11:56:52 +01:00
if (! laxVerifyForRepair )
2017-08-12 14:09:55 +01:00
{
2025-03-25 11:56:52 +01:00
var filesetsMissingVolumes = cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State)" )
2025-03-18 22:41:11 +01:00
. SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ())
. SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ())
. ExecuteScalarInt64 ( 0 );
2016-03-30 12:29:22 +02:00
2025-03-25 11:56:52 +01:00
if ( filesetsMissingVolumes != 0 )
{
if ( filesetsMissingVolumes == 1 )
using ( var reader = cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Timestamp"", ""VolumeID"" FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State)" )
. SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ())
. SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ())
. ExecuteReader ())
if ( reader . Read ())
throw new DatabaseInconsistencyException ( $"Detected 1 fileset with missing volume: FilesetId = {reader.ConvertValueToInt64(0)}, Time = ({ParseFromEpochSeconds(reader.ConvertValueToInt64(1))}), unmatched VolumeID {reader.ConvertValueToInt64(2)}" );
throw new DatabaseInconsistencyException ( $"Detected {filesetsMissingVolumes} filesets with missing volumes" );
}
2025-03-10 14:41:36 +01:00
2025-03-25 11:56:52 +01:00
var volumesMissingFilests = cmd . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" )
. SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ())
. SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ())
. ExecuteScalarInt64 ( 0 );
if ( volumesMissingFilests != 0 )
{
if ( volumesMissingFilests == 1 )
using ( var reader = cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Name"", ""State"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" != @State AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" )
. SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ())
. SetParameterValue ( "@State" , RemoteVolumeState . Deleted . ToString ())
. ExecuteReader ())
if ( reader . Read ())
throw new DatabaseInconsistencyException ( $"Detected 1 volume with missing filesets: VolumeId = {reader.ConvertValueToInt64(0)}, Name = {reader.ConvertValueToString(1)}, State = {reader.ConvertValueToString(2)}" );
throw new DatabaseInconsistencyException ( $"Detected {volumesMissingFilests} volumes with missing filesets" );
}
2025-03-10 14:41:36 +01:00
}
2025-03-10 21:02:16 +01:00
var nonAttachedFiles = cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup"")" );
if ( nonAttachedFiles != 0 )
2025-03-20 15:59:14 +01:00
{
// Attempt to create a better error message by finding the first 10 fileset ids with the issue
using var filesetIdReader = cmd . ExecuteReader ( @"SELECT DISTINCT(FilesetID) FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup"") LIMIT 11" );
var filesetIds = new HashSet < long >();
var overflow = false ;
while ( filesetIdReader . Read ())
{
if ( filesetIds . Count >= 10 )
{
overflow = true ;
break ;
}
filesetIds . Add ( filesetIdReader . ConvertValueToInt64 ( 0 ));
}
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})" );
// Fall back to a generic error message if we can't find the fileset ids
if (! pairs . Any ())
throw new DatabaseInconsistencyException ( $"Detected {nonAttachedFiles} file(s) in FilesetEntry without corresponding FileLookup entry" );
if ( overflow )
pairs = pairs . Append ( "... and more" );
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
if ( verifyfilelists )
{
2023-12-12 18:25:14 +01:00
var anyError = new List < string >();
2019-08-05 20:14:05 -04:00
using ( var cmd2 = m_connection . CreateCommand ( transaction ))
2025-03-10 21:02:16 +01:00
{
2019-08-05 20:14:05 -04:00
foreach ( var filesetid in cmd . ExecuteReaderEnumerable ( @"SELECT ""ID"" FROM ""Fileset"" " ). Select ( x => x . ConvertValueToInt64 ( 0 , - 1 )))
2018-10-17 14:37:23 +02:00
{
2025-03-14 12:13:17 +01:00
var expandedCmd = FormatInvariant ( $@"SELECT COUNT(*) FROM (SELECT DISTINCT ""Path"" FROM ({LocalDatabase.LIST_FILESETS}) UNION SELECT DISTINCT ""Path"" FROM ({LocalDatabase.LIST_FOLDERS_AND_SYMLINKS}))" );
2025-03-18 22:41:11 +01:00
var expandedlist = cmd2
. SetCommandAndParameters ( expandedCmd )
. SetParameterValue ( "@FilesetId" , filesetid )
. SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID )
. SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID )
. ExecuteScalarInt64 ( 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);
var storedlist = cmd2 . SetCommandAndParameters ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FilesetEntry"".""FilesetID"" = @FilesetId" )
. SetParameterValue ( "@FilesetId" , filesetid )
. ExecuteScalarInt64 ( 0 );
2019-08-05 20:14:05 -04:00
if ( expandedlist != storedlist )
{
var filesetname = filesetid . ToString ();
var fileset = FilesetTimes . Zip ( Enumerable . Range ( 0 , FilesetTimes . Count ()), ( a , b ) => new Tuple < long , long , DateTime >( b , a . Key , a . Value )). FirstOrDefault ( x => x . Item2 == filesetid );
if ( fileset != null )
2025-03-13 23:06:32 +01:00
filesetname = $"version {fileset.Item1}: {fileset.Item3} (database id: {fileset.Item2})" ;
anyError . Add ( $"Unexpected difference in fileset {filesetname}, found {expandedlist} entries, but expected {storedlist}" );
2019-08-05 20:14:05 -04:00
}
2018-10-17 14:37:23 +02:00
}
2025-03-10 21:02:16 +01:00
}
2024-06-21 07:52:07 +02:00
if ( anyError . Any ())
2023-12-12 18:25:14 +01:00
{
2025-03-10 14:37:30 +01:00
throw new DatabaseInconsistencyException ( string . Join ( "\n\r" , anyError ), "FilesetDifferences" );
2023-12-12 18:25:14 +01:00
}
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 ; }
}
internal class Block : IBlock
{
public string Hash { get ; private set ; }
public long Size { get ; private set ; }
public Block ( string hash , long size )
{
2025-03-14 14:34:56 +01:00
Hash = hash ;
Size = size ;
2019-08-05 20:14:05 -04:00
}
}
2013-04-27 15:13:14 +02:00
2025-04-03 14:20:00 +02:00
public IEnumerable < IBlock > GetBlocks ( long volumeid , IDbTransaction ? transaction = null )
2019-08-05 20:14:05 -04:00
{
2025-04-03 14:20:00 +02:00
using var cmd = m_connection . CreateCommand ( transaction )
. SetCommandAndParameters ( @"SELECT DISTINCT ""Hash"", ""Size"" FROM ""Block"" WHERE ""VolumeID"" = @VolumeId" )
. SetParameterValue ( "@VolumeId" , volumeid );
using ( var rd = cmd . ExecuteReader ())
2019-08-05 20:14:05 -04:00
while ( rd . Read ())
2025-04-03 14:20:00 +02:00
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-03-14 14:34:56 +01:00
private readonly IDataReader 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-03-14 14:34:56 +01:00
public BlocklistHashEnumerator ( BlocklistHashEnumerable parent , IDataReader 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 ;
if (! m_reader . Read ())
{
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-03-14 14:34:56 +01:00
private readonly IDataReader m_reader ;
2013-04-27 15:13:14 +02:00
2025-03-14 14:34:56 +01:00
public BlocklistHashEnumerable ( IDataReader 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
""L"".""Path"",
""L"".""Lastmodified"",
""L"".""Filelength"",
""L"".""Filehash"",
""L"".""Metahash"",
""L"".""Metalength"",
""L"".""BlocklistHash"",
""L"".""FirstBlockHash"",
""L"".""FirstBlockSize"",
""L"".""FirstMetaBlockHash"",
""L"".""FirstMetaBlockSize"",
""M"".""Hash"" AS ""MetaBlocklistHash""
FROM
(
SELECT
""J"".""Path"",
""J"".""Lastmodified"",
""J"".""Filelength"",
""J"".""Filehash"",
""J"".""Metahash"",
""J"".""Metalength"",
""K"".""Hash"" AS ""BlocklistHash"",
""J"".""FirstBlockHash"",
""J"".""FirstBlockSize"",
""J"".""FirstMetaBlockHash"",
""J"".""FirstMetaBlockSize"",
""J"".""MetablocksetID""
FROM
(
SELECT
2017-08-12 14:09:55 +01:00
""A"".""Path"" AS ""Path"",
""D"".""Lastmodified"" AS ""Lastmodified"",
""B"".""Length"" AS ""Filelength"",
""B"".""FullHash"" AS ""Filehash"",
""E"".""FullHash"" AS ""Metahash"",
""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""
2016-04-04 18:11:48 +02:00
FROM
2017-08-12 14:09:55 +01:00
""File"" A
LEFT JOIN ""Blockset"" B
ON ""A"".""BlocksetID"" = ""B"".""ID""
LEFT JOIN ""Metadataset"" C
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""
LEFT JOIN ""Block"" F
ON ""G"".""BlockID"" = ""F"".""ID""
LEFT JOIN ""BlocksetEntry"" I
ON ""E"".""ID"" = ""I"".""BlocksetID""
LEFT JOIN ""Block"" H
ON ""I"".""BlockID"" = ""H"".""ID""
2016-04-04 18:11:48 +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
2017-08-12 14:09:55 +01:00
(""I"".""Index"" = 0 OR ""I"".""Index"" IS NULL) AND
(""G"".""Index"" = 0 OR ""G"".""Index"" IS NULL)
2016-04-04 18:11:48 +02:00
) J
LEFT OUTER JOIN
""BlocklistHash"" K
ON
""K"".""BlocksetID"" = ""J"".""BlocksetID""
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
""FilesetEntry"" A,
""File"" B,
""Metadataset"" C,
""Blockset"" D,
""BlocksetEntry"" E,
""Block"" F
WHERE
""A"".""FileID"" = ""B"".""ID""
AND ""B"".""MetadataID"" = ""C"".""ID""
AND ""C"".""BlocksetID"" = ""D"".""ID""
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-03-14 14:34:56 +01:00
public void WriteFileset ( Volumes . FilesetVolumeWriter filesetvolume , long filesetId , IDbTransaction transaction )
2013-04-27 15:13:14 +02:00
{
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2013-04-27 15:13:14 +02:00
{
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( LIST_FOLDERS_AND_SYMLINKS )
. SetParameterValue ( "@FilesetId" , filesetId )
. SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID )
. SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID );
2013-04-27 15:13:14 +02:00
2025-04-03 14:20:00 +02:00
string? lastpath = null ;
2013-04-27 15:13:14 +02:00
using ( var rd = cmd . ExecuteReader ())
2019-08-05 20:14:05 -04:00
while ( rd . Read ())
{
var blocksetID = rd . ConvertValueToInt64 ( 0 , - 1 );
2025-04-03 14:32:47 +02:00
var path = rd . ConvertValueToString ( 2 );
2019-08-05 20:14:05 -04:00
var metalength = rd . ConvertValueToInt64 ( 3 , - 1 );
2025-04-03 14:32:47 +02:00
var metahash = rd . ConvertValueToString ( 4 );
var metablockhash = rd . ConvertValueToString ( 6 );
var metablocklisthash = rd . ConvertValueToString ( 7 );
2019-08-05 20:14:05 -04:00
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 });
}
2016-04-04 18:11:48 +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-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( LIST_FILESETS )
. SetParameterValue ( "@FilesetId" , filesetId );
2013-04-27 15:13:14 +02:00
using ( var rd = cmd . ExecuteReader ())
2019-08-05 20:14:05 -04:00
if ( rd . Read ())
2013-04-27 15:13:14 +02:00
{
2019-08-05 20:14:05 -04:00
var more = false ;
do
{
2025-04-03 14:20:00 +02:00
var path = rd . ConvertValueToString ( 0 );
var filehash = rd . ConvertValueToString ( 3 );
2019-08-05 20:14:05 -04:00
var size = rd . ConvertValueToInt64 ( 2 );
var lastmodified = new DateTime ( rd . ConvertValueToInt64 ( 1 , 0 ), DateTimeKind . Utc );
2025-04-03 14:20:00 +02:00
var metahash = rd . ConvertValueToString ( 4 );
2019-08-05 20:14:05 -04:00
var metasize = rd . ConvertValueToInt64 ( 5 , - 1 );
var p = rd . GetValue ( 6 );
var blrd = ( p == null || p == DBNull . Value ) ? null : new BlocklistHashEnumerable ( rd );
2025-04-03 14:20:00 +02:00
var blockhash = rd . ConvertValueToString ( 7 );
2019-08-05 20:14:05 -04:00
var blocksize = rd . ConvertValueToInt64 ( 8 , - 1 );
2025-04-03 14:20:00 +02:00
var metablockhash = rd . ConvertValueToString ( 9 );
2019-08-05 20:14:05 -04:00
//var metablocksize = rd.ConvertValueToInt64(10, -1);
2025-04-03 14:20:00 +02:00
var metablocklisthash = rd . ConvertValueToString ( 11 );
2019-08-05 20:14:05 -04:00
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 = rd . Read ();
else
more = blrd . MoreData ;
} while ( more );
}
2013-04-27 15:13:14 +02:00
}
}
2019-08-05 20:14:05 -04:00
2025-03-14 14:34:56 +01:00
public void LinkFilesetToVolume ( long filesetid , long volumeid , IDbTransaction transaction )
2025-03-10 14:37:30 +01:00
{
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2025-03-10 14:37:30 +01:00
{
2025-03-18 22:41:11 +01:00
var c = cmd . SetCommandAndParameters ( @"UPDATE ""Fileset"" SET ""VolumeID"" = @VolumeId WHERE ""ID"" = @FilesetId" )
. SetParameterValue ( "@VolumeId" , volumeid )
. SetParameterValue ( "@FilesetId" , filesetid )
. ExecuteNonQuery ();
2025-03-10 14:37:30 +01:00
if ( c != 1 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Failed to link filesetid {filesetid} to volumeid {volumeid}" );
2025-03-10 14:37:30 +01:00
}
}
2025-03-14 14:34:56 +01:00
public void PushTimestampChangesToPreviousVersion ( long filesetId , IDbTransaction 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-03-18 22:41:11 +01:00
using ( var cmd = m_connection . CreateCommand ( transaction , query ))
cmd . SetParameterValue ( "@FilesetId" , filesetId )
. ExecuteNonQuery ();
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
2013-05-11 12:03:15 +02:00
///</summary>
public class FilteredFilenameTable : IDisposable
{
public string Tablename { get ; private set ; }
2025-03-14 14:34:56 +01:00
private readonly IDbConnection m_connection ;
2018-05-23 21:18:01 -07:00
2025-04-03 15:46:20 +02:00
public FilteredFilenameTable ( IDbConnection connection , IFilter filter , IDbTransaction ? 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-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2024-06-21 07:52:07 +02:00
{
2025-03-14 12:13:17 +01:00
cmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" AS SELECT DISTINCT ""Path"" FROM ""File"" " ));
2024-06-21 07:52:07 +02:00
return ;
}
}
2025-03-14 14:34:56 +01:00
if ( type == FilterType . Regexp || type == FilterType . Group )
2013-05-11 12:03:15 +02:00
{
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2013-05-11 12:03:15 +02:00
{
2018-06-14 10:12:24 +02:00
// TODO: Optimize this to not rely on the "File" view, and not instantiate the paths in full
2025-03-14 12:13:17 +01:00
cmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" (""Path"" TEXT NOT NULL)" ));
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2013-05-11 12:03:15 +02:00
{
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( tr . Parent , FormatInvariant ( $@"INSERT INTO ""{Tablename}"" (""Path"") VALUES (@Path)" ));
2019-08-05 20:14:05 -04:00
using ( var c2 = m_connection . CreateCommand ())
using ( var rd = c2 . ExecuteReader ( @"SELECT DISTINCT ""Path"" FROM ""File"" " ))
while ( rd . Read ())
2013-05-11 12:03:15 +02:00
{
2025-04-03 14:32:47 +02:00
var p = rd . ConvertValueToString ( 0 );
2025-03-14 14:34:56 +01:00
if ( FilterExpression . Matches ( filter , p ))
2025-03-18 22:41:11 +01:00
cmd . SetParameterValue ( "@Path" , p )
. ExecuteNonQuery ();
2013-05-11 12:03:15 +02:00
}
2019-08-05 20:14:05 -04:00
2013-06-10 23:04:44 +02:00
tr . Commit ();
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
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2013-11-20 19:16:35 +01:00
{
2025-03-14 12:13:17 +01:00
cmd . ExecuteNonQuery ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{Tablename}"" (""Path"" TEXT NOT NULL)" ));
2025-03-18 22:41:11 +01:00
cmd . ExecuteNonQuery ( FormatInvariant ( $@"INSERT INTO ""{Tablename}"" SELECT DISTINCT ""Path"" FROM ""File"" WHERE {sb}" ), args );
2013-11-20 19:16:35 +01:00
tr . Commit ();
}
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
{
using ( var cmd = m_connection . CreateCommand ())
2025-03-14 12:13:17 +01:00
cmd . ExecuteNonQuery ( FormatInvariant ( @ $"DROP TABLE IF EXISTS ""{Tablename}"" " ));
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-04-23 15:40:16 +02:00
public void RenameRemoteFile ( string oldname , string newname , IDbTransaction ? transaction )
2013-07-23 18:56:01 +02:00
{
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2013-07-23 18:56:01 +02:00
{
//Rename the old entry, to preserve ID links
2025-03-18 22:41:11 +01:00
var c = cmd . SetCommandAndParameters ( @"UPDATE ""Remotevolume"" SET ""Name"" = @Newname WHERE ""Name"" = @Oldname" )
. SetParameterValue ( "@Newname" , newname )
. SetParameterValue ( "@Oldname" , oldname )
. ExecuteNonQuery ();
2013-07-23 18:56:01 +02:00
if ( c != 1 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Unexpected result from renaming \" { oldname } \ " to \"{newname}\", expected {1} got {c}" );
2019-08-05 20:14:05 -04:00
2013-07-23 18:56:01 +02:00
// Grab the type of entry
2025-03-18 22:41:11 +01:00
var type = ( RemoteVolumeType ) Enum . Parse (
typeof ( RemoteVolumeType ),
2025-03-19 15:32:05 +01:00
cmd . SetCommandAndParameters ( @"SELECT ""Type"" FROM ""Remotevolume"" WHERE ""Name"" = @Name" )
2025-03-18 22:41:11 +01:00
. SetParameterValue ( "@Name" , newname )
. ExecuteScalar ()
2025-04-03 14:20:00 +02:00
?. ToString () ?? "" ,
2025-03-18 22:41:11 +01:00
true );
2019-08-05 20:14:05 -04:00
2013-07-23 18:56:01 +02:00
//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
RegisterRemoteVolume ( oldname , type , RemoteVolumeState . Deleting , tr . Parent );
2019-08-05 20:14:05 -04:00
2013-07-23 18:56:01 +02:00
tr . Commit ();
}
}
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-04-03 14:20:00 +02:00
public virtual long CreateFileset ( long volumeid , DateTime timestamp , IDbTransaction ? transaction = null )
2013-07-22 16:54:19 +02:00
{
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2013-07-22 16:54:19 +02:00
{
2025-03-18 22:41:11 +01:00
var id = cmd . SetCommandAndParameters ( @"INSERT INTO ""Fileset"" (""OperationID"", ""Timestamp"", ""VolumeID"", ""IsFullBackup"") VALUES (@OperationId, @Timestamp, @VolumeId, @IsFullBackup); SELECT last_insert_rowid();" )
. SetParameterValue ( "@OperationId" , m_operationid )
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( timestamp ))
. SetParameterValue ( "@VolumeId" , volumeid )
. SetParameterValue ( "@IsFullBackup" , BackupType . PARTIAL_BACKUP )
. ExecuteScalarInt64 (- 1 );
2013-07-22 16:54:19 +02:00
tr . Commit ();
return id ;
}
}
2024-06-21 07:52:07 +02:00
2025-03-14 14:34:56 +01:00
public void AddIndexBlockLink ( long indexVolumeID , long blockVolumeID , IDbTransaction transaction )
2013-07-22 16:54:19 +02:00
{
2025-03-18 22:41:11 +01:00
m_insertIndexBlockLink . SetParameterValue ( "@IndexVolumeId" , indexVolumeID )
. SetParameterValue ( "@BlockVolumeId" , blockVolumeID )
. ExecuteNonQuery ( transaction );
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-04-03 14:20:00 +02:00
public IEnumerable < Tuple < string , byte [], int >> GetBlocklists ( long volumeid , long blocksize , int hashsize , IDbTransaction ? transaction = null )
2014-12-30 18:26:08 +01:00
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2014-12-30 18:26:08 +01:00
{
2023-11-30 21:45:46 +01:00
// Group subquery by hash to ensure that each blocklist hash appears only once in the result
2025-03-14 12:13:17 +01:00
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-03-14 12:13:17 +01: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""
ORDER BY ""A"".""BlocksetID"", ""B"".""Index""" );
2014-12-30 18:26:08 +01:00
2025-04-03 14:20:00 +02:00
string? curHash = null ;
var count = 0 ;
var buffer = new byte [ blocksize ];
2014-12-30 18:26:08 +01:00
2025-03-18 22:41:11 +01:00
using ( var rd = cmd . SetCommandAndParameters ( sql ). SetParameterValue ( "@VolumeId" , volumeid ). ExecuteReader ())
2014-12-30 18:26:08 +01:00
while ( rd . Read ())
{
2025-04-03 14:32:47 +02:00
var blockhash = rd . ConvertValueToString ( 0 );
2024-09-09 15:27:37 +02:00
if (( blockhash != curHash && curHash != null ) || count + hashsize > buffer . Length )
2014-12-30 18:26:08 +01:00
{
2025-04-03 14:20:00 +02:00
yield return new Tuple < string , byte [], int >( curHash !, buffer , count );
2018-06-06 08:37:09 +02:00
buffer = new byte [ blocksize ];
2024-09-09 15:27:37 +02:00
count = 0 ;
2014-12-30 18:26:08 +01:00
}
2025-04-03 14:20:00 +02:00
var hash = Convert . FromBase64String ( rd . ConvertValueToString ( 1 ) ?? throw new Exception ( "Hash is null" ));
2024-09-09 15:27:37 +02:00
Array . Copy ( hash , 0 , buffer , count , hashsize );
2014-12-30 18:26:08 +01:00
curHash = blockhash ;
2024-09-09 15:27:37 +02:00
count += hashsize ;
2014-12-30 18:26:08 +01:00
}
2016-02-23 20:56:24 +01:00
if ( curHash != null )
2024-09-09 15:27:37 +02:00
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-04-03 14:20:00 +02:00
public void UpdateFullBackupStateInFileset ( long fileSetId , bool isFullBackup , IDbTransaction ? transaction = null )
2019-09-01 13:12:03 -04:00
{
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2025-03-14 14:34:56 +01:00
using ( var cmd = m_connection . CreateCommand ( tr . Parent ))
2019-09-01 13:12:03 -04:00
{
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( @"UPDATE ""Fileset"" SET ""IsFullBackup"" = @IsFullBackup WHERE ""ID"" = @FilesetId;" )
. SetParameterValue ( "@FilesetId" , fileSetId )
. SetParameterValue ( "@IsFullBackup" , isFullBackup ? BackupType . FULL_BACKUP : BackupType . PARTIAL_BACKUP )
. ExecuteNonQuery ();
2019-09-01 13:12:03 -04:00
tr . Commit ();
}
}
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>
public void ClearFilesetEntries ( long filesetId , IDbTransaction transaction )
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
cmd . SetCommandAndParameters ( @"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" = @FilesetId" )
. SetParameterValue ( "@FilesetId" , filesetId )
. ExecuteNonQuery ();
}
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>
public RemoteVolumeEntry GetLastIncompleteFilesetVolume ( IDbTransaction transaction )
{
var candidates = GetIncompleteFilesets ( transaction ). OrderBy ( x => x . Value ). ToArray ();
if ( candidates . Any ())
return GetRemoteVolumeFromFilesetID ( candidates . Last (). Key , transaction );
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-04-03 15:46:20 +02:00
public IEnumerable < KeyValuePair < long , DateTime >> GetIncompleteFilesets ( IDbTransaction ? transaction )
2025-03-08 15:13:03 +01:00
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
2025-03-14 14:34:56 +01:00
using ( var rd = cmd . ExecuteReader ( 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 ( rd . Read ())
2019-09-01 13:12:03 -04:00
{
2025-03-14 14:34:56 +01:00
yield return new KeyValuePair < long , DateTime >(
2025-04-03 15:46:20 +02:00
rd . ConvertValueToInt64 ( 0 ),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime ()
2025-03-14 14:34:56 +01:00
);
2019-09-01 13:12:03 -04:00
}
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-04-03 14:20:00 +02:00
public RemoteVolumeEntry GetRemoteVolumeFromFilesetID ( long filesetID , IDbTransaction ? transaction = null )
2025-03-08 15:13:03 +01:00
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
2025-04-23 22:37:31 +02:00
using ( var rd = cmd . SetCommandAndParameters ( @"SELECT ""RemoteVolume"".""ID"", ""Name"", ""Type"", ""Size"", ""Hash"", ""State"", ""DeleteGraceTime"", ""ArchiveTime"" FROM ""RemoteVolume"", ""Fileset"" WHERE ""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""Fileset"".""ID"" = @FilesetId" )
2025-03-18 22:41:11 +01:00
. SetParameterValue ( "@FilesetId" , filesetID )
. ExecuteReader ())
2025-03-08 15:13:03 +01:00
if ( rd . Read ())
return new RemoteVolumeEntry (
rd . ConvertValueToInt64 ( 0 , - 1 ),
2025-04-03 14:20:00 +02:00
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 4 ),
2025-03-08 15:13:03 +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-04-23 22:37:31 +02:00
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 6 )). ToLocalTime (),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 7 )). ToLocalTime ()
2025-03-08 15:13:03 +01:00
);
else
2025-03-18 20:44:05 +01:00
return default ( RemoteVolumeEntry );
2019-09-01 13:12:03 -04:00
}
2015-02-15 23:26:52 +01:00
public void PurgeLogData ( DateTime threshold )
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ( tr ))
2015-02-15 23:26:52 +01:00
{
2019-09-01 09:56:14 -07:00
var t = Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( threshold );
2025-03-18 22:41:11 +01:00
cmd . SetCommandAndParameters ( @"DELETE FROM ""LogData"" WHERE ""Timestamp"" < @Timestamp" )
. SetParameterValue ( "@Timestamp" , t )
. ExecuteNonQuery ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""RemoteOperation"" WHERE ""Timestamp"" < @Timestamp" )
. SetParameterValue ( "@Timestamp" , t )
. ExecuteNonQuery ();
2015-02-15 23:26:52 +01:00
tr . Commit ();
}
}
2019-08-05 20:14:05 -04:00
2019-09-07 17:16:34 -04:00
public void PurgeDeletedVolumes ( DateTime threshold )
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2019-09-07 17:16:34 -04:00
using ( var cmd = m_connection . CreateCommand ( tr ))
{
2025-03-18 22:41:11 +01:00
m_removedeletedremotevolumeCommand . SetParameterValue ( "@Now" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( threshold ))
. ExecuteNonQuery ( tr );
2019-09-07 17:16:34 -04:00
tr . Commit ();
}
}
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-03-14 14:34:56 +01:00
DisposeAllFields < IDbCommand >( 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-04-14 12:04:00 +02:00
using ( var transaction = m_connection . BeginTransactionSafe ())
2025-01-28 08:55:28 +01:00
using ( var command = m_connection . CreateCommand ( transaction ))
2019-04-22 21:11:27 -07:00
{
2025-01-28 08:55:28 +01:00
// SQLite recommends that PRAGMA optimize is run just before closing each database connection.
command . ExecuteNonQuery ( "PRAGMA optimize" );
try
2019-04-22 21:11:27 -07:00
{
transaction . Commit ();
}
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
}
2016-04-06 20:40:34 +02:00
m_connection . Close ();
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-03-20 09:51:49 +01:00
public void 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 ();
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-04-03 14:20:00 +02:00
public long GetOrCreatePathPrefix ( string prefix , IDbTransaction ? 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-03-18 22:41:11 +01:00
var id = m_findpathprefixCommand . SetParameterValue ( "@Prefix" , prefix )
. ExecuteScalarInt64 ( transaction );
2018-06-14 10:12:24 +02:00
if ( id < 0 )
2025-03-18 22:41:11 +01:00
id = m_insertpathprefixCommand . SetParameterValue ( "@Prefix" , prefix )
. ExecuteScalarInt64 ( transaction );
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>
private static readonly char [] _pathseparators = new char [] {
2025-03-14 14:34:56 +01:00
Path . DirectorySeparatorChar ,
Path . AltDirectorySeparatorChar ,
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
}
}