2024-04-15 08:24:01 +02:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
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 ;
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 ));
2013-03-27 16:06:45 +01:00
protected readonly System . Data . IDbConnection m_connection ;
protected readonly long m_operationid = - 1 ;
private readonly System . Data . IDbCommand m_updateremotevolumeCommand ;
private readonly System . Data . IDbCommand m_selectremotevolumesCommand ;
private readonly System . Data . IDbCommand m_selectremotevolumeCommand ;
private readonly System . Data . IDbCommand m_removeremotevolumeCommand ;
2019-09-07 17:16:34 -04:00
private readonly System . Data . IDbCommand m_removedeletedremotevolumeCommand ;
2016-09-15 11:39:27 +02:00
private readonly System . Data . IDbCommand m_selectremotevolumeIdCommand ;
2013-03-27 16:06:45 +01:00
private readonly System . Data . IDbCommand m_createremotevolumeCommand ;
2016-03-24 16:31:07 +01:00
private readonly System . Data . IDbCommand m_selectduplicateRemoteVolumesCommand ;
2013-03-27 16:06:45 +01:00
private readonly System . Data . IDbCommand m_insertlogCommand ;
2013-03-08 22:24:54 +01:00
private readonly System . Data . IDbCommand m_insertremotelogCommand ;
2013-07-22 16:54:19 +02:00
private readonly System . Data . IDbCommand m_insertIndexBlockLink ;
2013-03-27 16:06:45 +01:00
2018-06-14 10:12:24 +02:00
private readonly System . Data . IDbCommand m_findpathprefixCommand ;
private readonly System . Data . IDbCommand m_insertpathprefixCommand ;
2013-08-22 20:52:54 +02:00
protected BasicResults m_result ;
2013-05-25 16:40:15 +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 ; }
internal System . Data . 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 ; }
2013-03-08 22:24:54 +01:00
protected static System . Data . IDbConnection CreateConnection ( string path )
{
2016-09-15 11:39:27 +02:00
path = System . IO . Path . GetFullPath ( path );
2013-03-08 22:24:54 +01:00
if (! System . IO . Directory . Exists ( System . IO . Path . GetDirectoryName ( path )))
System . IO . Directory . CreateDirectory ( System . IO . Path . GetDirectoryName ( path ));
2019-08-05 20:14:05 -04:00
2017-01-15 23:09:47 +01:00
var c = Duplicati . Library . SQLiteHelper . SQLiteLoader . LoadConnection ( path );
2013-03-08 22:24:54 +01:00
2020-12-28 12:18:58 -08:00
try
{
Library . SQLiteHelper . DatabaseUpgrader . UpgradeDatabase ( c , path , typeof ( LocalDatabase ));
}
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 ;
}
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 )
{
this . OperationTimestamp = db . OperationTimestamp ;
this . m_connection = db . m_connection ;
this . m_operationid = db . m_operationid ;
2013-08-22 20:52:54 +02:00
this . m_result = db . m_result ;
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>
2013-03-27 16:06:45 +01:00
public LocalDatabase ( System . Data . IDbConnection connection , string operation )
2016-09-15 11:39:27 +02:00
: this ( connection )
2013-03-27 16:06:45 +01:00
{
2013-03-08 22:24:54 +01:00
this . OperationTimestamp = DateTime . UtcNow ;
m_connection = connection ;
if ( m_connection . State != System . Data . 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 ())
m_operationid = cmd . ExecuteScalarInt64 ( @"INSERT INTO ""Operation"" (""Description"", ""Timestamp"") VALUES (?, ?); SELECT last_insert_rowid();" , - 1 , operation , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( OperationTimestamp ));
}
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" ))
{
if (! rd . Read ())
{
throw new Exception ( "LocalDatabase does not contain a previous operation." );
}
m_operationid = rd . GetInt64 ( 0 );
OperationTimestamp = ParseFromEpochSeconds ( rd . GetInt64 ( 1 ));
}
}
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2016-09-15 11:39:27 +02:00
private LocalDatabase ( System . Data . IDbConnection connection )
{
2013-04-04 20:34:26 +02:00
m_updateremotevolumeCommand = connection . CreateCommand ();
m_selectremotevolumesCommand = connection . CreateCommand ();
2016-03-24 16:31:07 +01:00
m_selectduplicateRemoteVolumesCommand = connection . CreateCommand ();
2013-04-04 20:34:26 +02:00
m_selectremotevolumeCommand = connection . CreateCommand ();
m_insertlogCommand = connection . CreateCommand ();
m_insertremotelogCommand = connection . CreateCommand ();
m_removeremotevolumeCommand = connection . CreateCommand ();
2019-09-07 17:16:34 -04:00
m_removedeletedremotevolumeCommand = connection . CreateCommand ();
2016-09-15 11:39:27 +02:00
m_selectremotevolumeIdCommand = connection . CreateCommand ();
m_createremotevolumeCommand = connection . CreateCommand ();
2013-07-22 16:54:19 +02:00
m_insertIndexBlockLink = connection . CreateCommand ();
2018-06-14 10:12:24 +02:00
m_findpathprefixCommand = connection . CreateCommand ();
m_insertpathprefixCommand = connection . CreateCommand ();
2013-03-27 16:06:45 +01:00
m_insertlogCommand . CommandText = @"INSERT INTO ""LogData"" (""OperationID"", ""Timestamp"", ""Type"", ""Message"", ""Exception"") VALUES (?, ?, ?, ?, ?)" ;
2013-04-04 20:34:26 +02:00
m_insertlogCommand . AddParameters ( 5 );
2013-03-27 16:06:45 +01:00
m_insertremotelogCommand . CommandText = @"INSERT INTO ""RemoteOperation"" (""OperationID"", ""Timestamp"", ""Operation"", ""Path"", ""Data"") VALUES (?, ?, ?, ?, ?)" ;
2013-04-04 20:34:26 +02:00
m_insertremotelogCommand . AddParameters ( 5 );
2013-03-27 16:06:45 +01:00
m_updateremotevolumeCommand . CommandText = @"UPDATE ""Remotevolume"" SET ""OperationID"" = ?, ""State"" = ?, ""Hash"" = ?, ""Size"" = ? WHERE ""Name"" = ?" ;
2013-04-04 20:34:26 +02:00
m_updateremotevolumeCommand . AddParameters ( 5 );
2013-03-27 16:06:45 +01:00
2016-02-22 21:27:12 +01:00
m_selectremotevolumesCommand . CommandText = @"SELECT ""ID"", ""Name"", ""Type"", ""Size"", ""Hash"", ""State"", ""DeleteGraceTime"" FROM ""Remotevolume""" ;
2013-03-27 16:06:45 +01:00
2016-02-22 21:27:12 +01:00
m_selectremotevolumeCommand . CommandText = m_selectremotevolumesCommand . CommandText + @" WHERE ""Name"" = ?" ;
2024-04-15 08:24:01 +02:00
m_selectduplicateRemoteVolumesCommand . CommandText = string . Format ( @"SELECT DISTINCT ""Name"", ""State"" FROM ""Remotevolume"" WHERE ""Name"" IN (SELECT ""Name"" FROM ""Remotevolume"" WHERE ""State"" IN ('{0}', '{1}')) AND NOT ""State"" IN ('{0}', '{1}')" , RemoteVolumeState . Deleted . ToString (), RemoteVolumeState . Deleting . ToString ());
2016-03-24 16:31:07 +01:00
2013-03-27 16:06:45 +01:00
m_selectremotevolumeCommand . AddParameter ();
2017-09-18 11:53:50 +02:00
m_removeremotevolumeCommand . CommandText = @"DELETE FROM ""Remotevolume"" WHERE ""Name"" = ? AND (""DeleteGraceTime"" < ? OR ""State"" != ?)" ;
m_removeremotevolumeCommand . AddParameters ( 3 );
2013-03-27 16:06:45 +01:00
2024-04-15 08:24:01 +02:00
m_removedeletedremotevolumeCommand . CommandText = $@"DELETE FROM ""Remotevolume"" WHERE ""State"" == '{RemoteVolumeState.Deleted.ToString()}' AND (""DeleteGraceTime"" < ? OR LENGTH(""DeleteGraceTime"") > 12) " ; // >12 is to handle removal of old records that were in ticks
2019-09-07 17:16:34 -04:00
m_removedeletedremotevolumeCommand . AddParameter ();
2016-09-15 11:39:27 +02:00
m_selectremotevolumeIdCommand . CommandText = @"SELECT ""ID"" FROM ""Remotevolume"" WHERE ""Name"" = ?" ;
2013-03-27 16:06:45 +01:00
2016-09-15 11:39:27 +02:00
m_createremotevolumeCommand . CommandText = @"INSERT INTO ""Remotevolume"" (""OperationID"", ""Name"", ""Type"", ""State"", ""Size"", ""VerificationCount"", ""DeleteGraceTime"") VALUES (?, ?, ?, ?, ?, ?, ?); SELECT last_insert_rowid();" ;
2015-08-24 10:50:47 +01:00
m_createremotevolumeCommand . AddParameters ( 7 );
2013-07-22 16:54:19 +02:00
m_insertIndexBlockLink . CommandText = @"INSERT INTO ""IndexBlockLink"" (""IndexVolumeID"", ""BlockVolumeID"") VALUES (?, ?)" ;
m_insertIndexBlockLink . AddParameters ( 2 );
2018-06-14 10:12:24 +02:00
m_findpathprefixCommand . CommandText = @"SELECT ""ID"" FROM ""PathPrefix"" WHERE ""Prefix"" = ?" ;
m_findpathprefixCommand . AddParameter ();
m_insertpathprefixCommand . CommandText = @"INSERT INTO ""PathPrefix"" (""Prefix"") VALUES (?); SELECT last_insert_rowid(); " ;
m_insertpathprefixCommand . AddParameter ();
2016-09-15 11:39:27 +02:00
}
2013-05-25 16:40:15 +02:00
internal void SetResult ( BasicResults result )
{
m_result = result ;
}
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
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string hash , System . Data . 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
2016-03-24 16:30:19 +01:00
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string hash , bool suppressCleanup , System . Data . IDbTransaction transaction = null )
{
2019-08-05 20:14:05 -04:00
UpdateRemoteVolume ( name , state , size , hash , suppressCleanup , new TimeSpan ( 0 ), transaction );
2016-03-24 16:30:19 +01:00
}
public void UpdateRemoteVolume ( string name , RemoteVolumeState state , long size , string hash , bool suppressCleanup , TimeSpan deleteGraceTime , System . Data . IDbTransaction transaction = null )
2013-03-27 16:06:45 +01:00
{
2013-04-09 20:43:27 +02:00
m_updateremotevolumeCommand . Transaction = transaction ;
m_updateremotevolumeCommand . SetParameterValue ( 0 , m_operationid );
m_updateremotevolumeCommand . SetParameterValue ( 1 , state . ToString ());
m_updateremotevolumeCommand . SetParameterValue ( 2 , hash );
m_updateremotevolumeCommand . SetParameterValue ( 3 , size );
m_updateremotevolumeCommand . SetParameterValue ( 4 , name );
2019-09-07 17:16:34 -04:00
2013-04-09 20:43:27 +02:00
var c = m_updateremotevolumeCommand . 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
{
if (( c = cmd . ExecuteNonQuery (
@"UPDATE ""RemoteVolume"" SET ""DeleteGraceTime"" = ? WHERE ""Name"" = ? " ,
Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow + deleteGraceTime ),
name )) != 1 )
{
throw new Exception ( $"Unexpected number of updates when recording remote volume updates: {c}!" );
}
}
}
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
}
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 ())
2019-11-17 09:30:19 -08: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 ())
2019-11-17 09:30:19 -08:00
yield return new KeyValuePair < long , DateTime >( rd . GetInt64 ( 0 ), ParseFromEpochSeconds ( rd . GetInt64 ( 1 )). ToLocalTime ());
2013-05-11 22:56:21 +02:00
}
}
2015-04-08 21:01:36 +02:00
public Tuple < string , object []> GetFilelistWhereClause ( DateTime time , long [] versions , IEnumerable < KeyValuePair < long , DateTime >> filesetslist = null , bool singleTimeMatch = false )
2016-09-15 11:39:27 +02:00
{
var filesets = ( filesetslist ?? this . FilesetTimes ). ToArray ();
2018-05-13 15:29:55 -07:00
StringBuilder query = new StringBuilder ();
2016-09-15 11:39:27 +02:00
var args = new List < 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
query . Append ( singleTimeMatch ? @" ""Timestamp"" = ?" : @" ""Timestamp"" <= ?" );
2013-08-20 21:37:30 +02:00
// Make sure the resolution is the same (i.e. no milliseconds)
2019-09-01 09:56:14 -07:00
args . Add ( 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 )
{
2018-05-13 15:29:55 -07:00
StringBuilder 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 )
{
args . Add ( filesets [ v ]. Key );
2018-05-13 15:29:55 -07:00
qs . Append ( "?," );
2013-05-20 13:48:44 +02:00
}
2013-08-06 22:57:03 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "SkipInvalidVersion" , null , "Skipping invalid version: {0}" , v );
2018-05-12 17:24:33 -07:00
}
2018-05-13 15:29:55 -07:00
2013-05-20 13:48:44 +02:00
if ( qs . Length > 0 )
{
2013-06-20 20:17:10 +02:00
if ( hasTime )
2018-05-13 15:29:55 -07:00
query . Append ( " OR " );
query . Append ( @" ""ID"" IN (" + qs . ToString ( 0 , qs . Length - 1 ) + ")" );
2013-05-20 13:48:44 +02:00
}
}
2013-08-06 22:57:03 +02:00
2018-05-13 15:29:55 -07:00
if ( query . Length > 0 )
{
query . Insert ( 0 , " WHERE " );
}
2013-05-20 13:48:44 +02:00
}
2019-08-05 20:14:05 -04:00
2018-05-13 15:29:55 -07:00
return new Tuple < string , object []>( query . ToString (), args . ToArray ());
2013-05-20 13:48:44 +02:00
}
2013-04-22 21:06:52 +02:00
public long GetRemoteVolumeID ( string file , System . Data . IDbTransaction transaction = null )
2016-09-15 11:39:27 +02:00
{
m_selectremotevolumeIdCommand . Transaction = transaction ;
return m_selectremotevolumeIdCommand . ExecuteScalarInt64 ( null , - 1 , file );
}
2013-03-27 16:06:45 +01:00
2016-02-22 21:27:12 +01:00
public RemoteVolumeEntry GetRemoteVolume ( string file , System . Data . IDbTransaction transaction = null )
2013-03-27 16:06:45 +01:00
{
2016-02-22 21:27:12 +01:00
m_selectremotevolumeCommand . Transaction = transaction ;
2013-03-27 16:06:45 +01:00
m_selectremotevolumeCommand . SetParameterValue ( 0 , 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 ),
rd . GetValue ( 1 ). ToString (),
( rd . GetValue ( 4 ) == null || rd . GetValue ( 4 ) == DBNull . Value ) ? null : rd . GetValue ( 4 ). ToString (),
rd . ConvertValueToInt64 ( 3 , - 1 ),
( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), rd . GetValue ( 2 ). ToString ()),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . GetValue ( 5 ). ToString ()),
new DateTime ( rd . ConvertValueToInt64 ( 6 , 0 ), DateTimeKind . Utc )
);
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
}
2016-03-30 01:30:51 +02:00
public IEnumerable < KeyValuePair < string , RemoteVolumeState >> DuplicateRemoteVolumes ()
2016-03-24 16:31:07 +01:00
{
2019-08-05 20:14:05 -04:00
foreach ( var rd in m_selectduplicateRemoteVolumesCommand . ExecuteReaderEnumerable ( null ))
2016-03-24 16:31:07 +01:00
{
2016-03-30 01:30:51 +02:00
yield return new KeyValuePair < string , RemoteVolumeState >(
2016-03-24 16:31:07 +01:00
rd . GetValue ( 0 ). ToString (),
2016-03-30 01:30:51 +02:00
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . GetValue ( 1 ). ToString ())
2016-03-24 16:31:07 +01:00
);
}
}
2016-02-22 21:27:12 +01:00
public IEnumerable < RemoteVolumeEntry > GetRemoteVolumes ( System . Data . 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 ),
rd . GetValue ( 1 ). ToString (),
( rd . GetValue ( 4 ) == null || rd . GetValue ( 4 ) == DBNull . Value ) ? null : rd . GetValue ( 4 ). ToString (),
rd . ConvertValueToInt64 ( 3 , - 1 ),
( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), rd . GetValue ( 2 ). ToString ()),
( RemoteVolumeState ) Enum . Parse ( typeof ( RemoteVolumeState ), rd . GetValue ( 5 ). ToString ()),
new DateTime ( rd . ConvertValueToInt64 ( 6 , 0 ), DateTimeKind . Utc )
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>
2013-04-09 20:43:27 +02:00
public void LogRemoteOperation ( string operation , string path , string data , System . Data . IDbTransaction transaction )
2013-03-27 16:06:45 +01:00
{
2016-09-15 11:39:27 +02:00
m_insertremotelogCommand . Transaction = transaction ;
2013-04-09 20:43:27 +02:00
m_insertremotelogCommand . SetParameterValue ( 0 , m_operationid );
2019-09-01 09:56:14 -07:00
m_insertremotelogCommand . SetParameterValue ( 1 , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
2013-04-09 20:43:27 +02:00
m_insertremotelogCommand . SetParameterValue ( 2 , operation );
m_insertremotelogCommand . SetParameterValue ( 3 , path );
m_insertremotelogCommand . SetParameterValue ( 4 , data );
m_insertremotelogCommand . ExecuteNonQuery ();
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>
2013-04-09 20:43:27 +02:00
public void LogMessage ( string type , string message , Exception exception , System . Data . IDbTransaction transaction )
2013-03-27 16:06:45 +01:00
{
2016-09-15 11:39:27 +02:00
m_insertlogCommand . Transaction = transaction ;
2013-04-09 20:43:27 +02:00
m_insertlogCommand . SetParameterValue ( 0 , m_operationid );
2019-09-01 09:56:14 -07:00
m_insertlogCommand . SetParameterValue ( 1 , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
2013-04-09 20:43:27 +02:00
m_insertlogCommand . SetParameterValue ( 2 , type );
m_insertlogCommand . SetParameterValue ( 3 , message );
m_insertlogCommand . SetParameterValue ( 4 , exception == null ? null : exception . ToString ());
m_insertlogCommand . ExecuteNonQuery ();
2013-03-27 16:06:45 +01:00
}
2016-03-24 16:31:07 +01:00
public void UnlinkRemoteVolume ( string name , RemoteVolumeState state , System . Data . IDbTransaction transaction = null )
{
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
2016-03-24 16:31:07 +01:00
{
cmd . Transaction = tr . Parent ;
var c = cmd . ExecuteNonQuery ( @"DELETE FROM ""RemoteVolume"" WHERE ""Name"" = ? AND ""State"" = ? " , name , state . ToString ());
if ( c != 1 )
throw new Exception ( string . Format ( "Unexpected number of remote volumes deleted: {0}, expected {1}" , c , 1 ));
tr . Commit ();
}
}
2013-03-27 16:06:45 +01:00
public void RemoveRemoteVolume ( string name , System . Data . IDbTransaction transaction = null )
{
2016-03-16 00:49:28 +01:00
RemoveRemoteVolumes ( new string [] { name }, transaction );
}
2016-12-24 11:42:26 +01:00
public void RemoveRemoteVolumes ( IEnumerable < string > names , System . Data . 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 ))
using ( var deletecmd = m_connection . CreateCommand ())
2013-03-27 16:06:45 +01:00
{
2013-04-09 20:43:27 +02:00
deletecmd . Transaction = tr . Parent ;
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.
deletecmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMP TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY)" , volidstable ));
deletecmd . CommandText = string . Format ( @"INSERT OR IGNORE INTO ""{0}"" (""ID"") VALUES (?)" , volidstable );
deletecmd . Parameters . Clear ();
deletecmd . AddParameters ( 1 );
foreach ( var name in names )
{
var volumeid = GetRemoteVolumeID ( name , tr . Parent );
deletecmd . SetParameterValue ( 0 , volumeid );
deletecmd . ExecuteNonQuery ();
}
var volIdsSubQuery = string . Format ( @"SELECT ""ID"" FROM ""{0}"" " , volidstable );
deletecmd . Parameters . Clear ();
2019-08-05 20:14:05 -04:00
2016-03-16 00:49:28 +01:00
var bsIdsSubQuery = string . Format (
2020-02-23 13:23:21 -06:00
@"SELECT DISTINCT ""BlocksetEntry"".""BlocksetID"" FROM ""BlocksetEntry"", ""Block"""
2016-03-16 00:49:28 +01:00
+ @" WHERE ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""Block"".""VolumeID"" IN ({0}) "
+ @"UNION ALL "
2020-02-23 13:23:21 -06:00
+ @"SELECT DISTINCT ""BlocksetID"" FROM ""BlocklistHash"""
+ @" WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"" WHERE ""VolumeID"" IN ({0}))"
2016-03-16 00:49:28 +01:00
, 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).
deletecmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMP TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY)" , blocksetidstable ));
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"INSERT OR IGNORE INTO ""{0}"" (""ID"") {1}" , blocksetidstable , bsIdsSubQuery ));
2020-02-23 13:23:21 -06:00
bsIdsSubQuery = string . Format ( @"SELECT DISTINCT ""ID"" FROM ""{0}"" " , 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
var metadataFilesetQuery = $@"SELECT Metadataset.ID, FilesetEntry.FilesetID
FROM Metadataset
INNER JOIN FileLookup ON FileLookup.MetadataID = Metadataset.ID
INNER JOIN FilesetEntry ON FilesetEntry.FileID = FileLookup.ID
WHERE Metadataset.BlocksetID IN ({bsIdsSubQuery})
OR Metadataset.ID IN (SELECT MetadataID FROM FileLookup WHERE BlocksetID IN ({bsIdsSubQuery}))" ;
var metadataFilesetTable = @"DelMetadataFilesetIds-" + temptransguid ;
deletecmd . ExecuteNonQuery ( $@"CREATE TEMP TABLE ""{metadataFilesetTable}"" (MetadataID INTEGER PRIMARY KEY, FilesetID INTEGER)" );
deletecmd . ExecuteNonQuery ( $@"INSERT OR IGNORE INTO ""{metadataFilesetTable}"" (MetadataID, FilesetID) {metadataFilesetQuery}" );
// Delete FilesetEntry rows that had their metadata deleted
deletecmd . ExecuteNonQuery ( $@"DELETE FROM FilesetEntry
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
WHERE FileLookup.MetadataID IN (SELECT MetadataID FROM ""{metadataFilesetTable}""))" );
// Delete FilesetEntry rows that had their blocks deleted
deletecmd . ExecuteNonQuery ( $@"DELETE FROM FilesetEntry WHERE FilesetEntry.FileID IN (
SELECT ID FROM FileLookup
WHERE FileLookup.BlocksetID IN ({bsIdsSubQuery}))" );
deletecmd . ExecuteNonQuery ( $@"DELETE FROM FileLookup WHERE FileLookup.MetadataID IN (SELECT MetadataID FROM ""{metadataFilesetTable}"")" );
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" IN ({0})" , bsIdsSubQuery ));
2020-02-23 13:23:21 -06:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""FileLookup"" WHERE ""BlocksetID"" IN ({0})" , bsIdsSubQuery ));
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""Blockset"" WHERE ""ID"" IN ({0})" , bsIdsSubQuery ));
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""BlocksetEntry"" WHERE ""BlocksetID"" IN ({0})" , bsIdsSubQuery ));
2020-01-05 21:43:56 -06:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""BlocklistHash"" WHERE ""BlocklistHash"".""BlocksetID"" IN ({0})" , 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
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""IndexBlockLink"" WHERE ""BlockVolumeID"" IN ({0}) OR ""IndexVolumeID"" IN ({0})" , volIdsSubQuery ));
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""Block"" WHERE ""VolumeID"" IN ({0})" , volIdsSubQuery ));
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""DeletedBlock"" WHERE ""VolumeID"" IN ({0})" , volIdsSubQuery ));
2020-02-23 13:23:21 -06:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""ChangeJournalData"" WHERE ""FilesetID"" IN (SELECT ""ID"" FROM ""Fileset"" WHERE ""VolumeID"" IN ({0}))" , volIdsSubQuery ));
deletecmd . ExecuteNonQuery ( $@"DELETE FROM FilesetEntry WHERE FilesetID IN (SELECT ID FROM Fileset WHERE VolumeID IN ({volIdsSubQuery}))" );
deletecmd . ExecuteNonQuery ( $@"DELETE FROM Fileset WHERE VolumeID IN ({volIdsSubQuery})" );
// Delete from Fileset if FilesetEntry rows were deleted by related metadata and there are no references in FilesetEntry anymore
deletecmd . ExecuteNonQuery ( $@"DELETE FROM Fileset WHERE Fileset.ID IN
(SELECT DISTINCT FilesetID FROM ""{metadataFilesetTable}"")
AND Fileset.ID NOT IN
(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)
// System.Data.SQLite.SQLiteException (0x80004005): database table is locked
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""{0}"" " , blocksetidstable ));
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""{0}"" " , volidstable ));
2016-03-13 13:23:06 +01:00
try
{
deletecmd . CommandTimeout = 2 ;
deletecmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , blocksetidstable ));
2016-03-16 00:49:28 +01:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , volidstable ));
2020-02-23 13:23:21 -06:00
deletecmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , 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 ;
2019-09-07 17:16:34 -04:00
m_removeremotevolumeCommand . SetParameterValue ( 1 , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ));
2019-08-05 20:14:05 -04:00
m_removeremotevolumeCommand . SetParameterValue ( 2 , RemoteVolumeState . Deleted . ToString ());
foreach ( var name in names )
2016-03-16 00:49:28 +01:00
{
m_removeremotevolumeCommand . SetParameterValue ( 0 , name );
m_removeremotevolumeCommand . ExecuteNonQuery ();
}
2019-09-07 17:16:34 -04:00
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 ()
{
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
}
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , System . Data . IDbTransaction transaction )
{
return RegisterRemoteVolume ( name , type , state , new TimeSpan ( 0 ), transaction );
}
2015-08-24 10:50:47 +01:00
2015-04-05 14:33:13 +02:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , TimeSpan deleteGraceTime , System . Data . 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
2015-08-24 10:50:47 +01:00
public long RegisterRemoteVolume ( string name , RemoteVolumeType type , RemoteVolumeState state , long size , TimeSpan deleteGraceTime , System . Data . 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
{
2013-04-09 20:43:27 +02:00
m_createremotevolumeCommand . SetParameterValue ( 0 , m_operationid );
m_createremotevolumeCommand . SetParameterValue ( 1 , name );
m_createremotevolumeCommand . SetParameterValue ( 2 , type . ToString ());
m_createremotevolumeCommand . SetParameterValue ( 3 , state . ToString ());
2015-08-24 10:50:47 +01:00
m_createremotevolumeCommand . SetParameterValue ( 4 , size );
m_createremotevolumeCommand . SetParameterValue ( 5 , 0 );
2015-04-05 14:33:13 +02:00
if ( deleteGraceTime . Ticks <= 0 )
2015-08-24 10:50:47 +01:00
m_createremotevolumeCommand . SetParameterValue ( 6 , 0 );
2015-04-05 14:33:13 +02:00
else
2015-08-24 10:50:47 +01:00
m_createremotevolumeCommand . SetParameterValue ( 6 , ( DateTime . UtcNow + deleteGraceTime ). Ticks );
2019-08-05 20:14:05 -04:00
2013-04-09 20:43:27 +02:00
m_createremotevolumeCommand . Transaction = tr . Parent ;
2015-01-24 21:59:53 +01:00
var r = m_createremotevolumeCommand . ExecuteScalarInt64 ();
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
2013-08-24 22:27:30 +02:00
var tmp = GetFilelistWhereClause ( restoretime , versions );
string query = tmp . Item1 ;
var args = tmp . Item2 ;
2013-05-20 13:48:44 +02:00
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 ())
{
using ( var rd = cmd . ExecuteReader ( @"SELECT ""ID"" FROM ""Fileset"" " + query + @" ORDER BY ""Timestamp"" DESC" , args ))
2013-08-24 22:27:30 +02:00
while ( rd . Read ())
2015-01-24 21:59:53 +01:00
res . Add ( rd . GetInt64 ( 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 ())
res . Add ( rd . GetInt64 ( 0 ));
return res ;
}
2019-08-05 20:14:05 -04:00
public bool IsFilesetFullBackup ( DateTime filesetTime )
{
using ( var cmd = m_connection . CreateCommand ())
{
2019-09-07 11:27:20 -04:00
using ( var rd = cmd . ExecuteReader ( $@"SELECT ""IsFullBackup"" FROM ""Fileset"" WHERE ""Timestamp"" = {Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(filesetTime)}" ))
2019-08-05 20:14:05 -04:00
{
2019-08-16 00:37:52 -04:00
if (! rd . Read ())
{
return false ;
}
2019-08-05 20:14:05 -04:00
var isFullBackup = rd . GetInt32 ( 0 );
2019-08-18 14:41:05 -04:00
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
2013-03-08 22:24:54 +01:00
public System . Data . IDbTransaction BeginTransaction ()
{
return m_connection . BeginTransaction ();
}
protected class TemporaryTransactionWrapper : IDisposable
{
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDbTransaction m_parent ;
private readonly bool m_isTemporary ;
2013-03-08 22:24:54 +01:00
public TemporaryTransactionWrapper ( System . Data . IDbConnection connection , System . Data . IDbTransaction transaction )
{
if ( transaction != null )
{
m_parent = transaction ;
m_isTemporary = false ;
}
else
{
m_parent = connection . BeginTransaction ();
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 ();
}
public System . Data . IDbTransaction Parent { get { return m_parent ; } }
2013-03-27 16:06:45 +01:00
}
2019-08-05 20:14:05 -04:00
2015-04-08 20:33:30 +02:00
private IEnumerable < KeyValuePair < string , string >> GetDbOptionList ( System . Data . 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 ())
yield return new KeyValuePair < string , string >( rd . GetValue ( 0 ). ToString (), rd . GetValue ( 1 ). ToString ());
2016-09-15 11:39:27 +02:00
}
2019-08-05 20:14:05 -04:00
2015-04-08 20:33:30 +02:00
public IDictionary < string , string > GetDbOptions ( System . Data . 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
public bool RepairInProgress
{
get
{
return GetDbOptions (). ContainsKey ( "repair-in-progress" );
}
set
{
var opts = GetDbOptions ();
if ( value )
opts [ "repair-in-progress" ] = "true" ;
else
opts . Remove ( "repair-in-progress" );
2019-08-05 20:14:05 -04:00
2016-03-18 13:26:12 +01:00
SetDbOptions ( opts );
}
}
2016-09-13 21:55:15 +02:00
2016-09-15 11:39:27 +02:00
public bool PartiallyRecreated
{
get
{
return GetDbOptions (). ContainsKey ( "partially-recreated" );
}
set
{
var opts = GetDbOptions ();
if ( value )
opts [ "partially-recreated" ] = "true" ;
else
opts . Remove ( "partially-recreated" );
SetDbOptions ( opts );
}
}
2019-08-05 20:14:05 -04:00
2016-09-15 11:39:27 +02:00
public void SetDbOptions ( IDictionary < string , string > options , System . Data . IDbTransaction transaction = null )
{
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
using ( var cmd = m_connection . CreateCommand ())
2016-09-15 11:39:27 +02:00
{
cmd . Transaction = tr . Parent ;
cmd . ExecuteNonQuery ( @"DELETE FROM ""Configuration"" " );
2019-08-05 20:14:05 -04:00
foreach ( var kp in options )
2016-09-15 11:39:27 +02:00
cmd . ExecuteNonQuery ( @"INSERT INTO ""Configuration"" (""Key"", ""Value"") VALUES (?, ?) " , kp . Key , kp . Value );
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 ())
2016-09-15 11:39:27 +02:00
return cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM ""Block"" WHERE ""Size"" > ?" , - 1 , fhblocksize );
}
2013-03-31 19:33:34 +02:00
2016-10-18 13:27:36 +02:00
public void VerifyConsistency ( long blocksize , long hashsize , bool verifyfilelists , System . Data . 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." );
2016-09-15 11:39:27 +02:00
throw new InvalidDataException ( sb . ToString ());
}
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 )
throw new InvalidDataException ( string . Format ( "Found {0} blocklist hashes, but there should be {1}. Run repair to fix it." , real_count , unique_count ));
2015-02-03 23:47:33 +01:00
var itemswithnoblocklisthash = cmd . ExecuteScalarInt64 ( string . Format ( @"SELECT COUNT(*) FROM (SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {0} - 1) / {0}) 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"")" , blocksize / hashsize ), 0 );
if ( itemswithnoblocklisthash != 0 )
throw new InvalidDataException ( string . Format ( "Found {0} file(s) with missing blocklist hashes" , itemswithnoblocklisthash ));
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 )
2017-08-12 14:09:55 +01:00
{
throw new Exception ( "Detected non-empty blocksets with no associated blocks!" );
}
2016-03-30 12:29:22 +02:00
2018-06-14 10:12:24 +02:00
if ( cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM ""FileLookup"" WHERE ""BlocksetID"" != ? AND ""BlocksetID"" != ? AND NOT ""BlocksetID"" IN (SELECT ""ID"" FROM ""Blockset"")" , 0 , FOLDER_BLOCKSET_ID , SYMLINK_BLOCKSET_ID ) != 0 )
2017-08-12 14:09:55 +01:00
throw new Exception ( "Detected files associated with non-existing blocksets!" );
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 ))
foreach ( var filesetid in cmd . ExecuteReaderEnumerable ( @"SELECT ""ID"" FROM ""Fileset"" " ). Select ( x => x . ConvertValueToInt64 ( 0 , - 1 )))
2018-10-17 14:37:23 +02:00
{
2019-08-05 20:14:05 -04:00
var expandedCmd = string . Format ( @"SELECT COUNT(*) FROM (SELECT DISTINCT ""Path"" FROM ({0}) UNION SELECT DISTINCT ""Path"" FROM ({1}))" , LocalDatabase . LIST_FILESETS , LocalDatabase . LIST_FOLDERS_AND_SYMLINKS );
var expandedlist = cmd2 . ExecuteScalarInt64 ( expandedCmd , 0 , filesetid , FOLDER_BLOCKSET_ID , SYMLINK_BLOCKSET_ID , filesetid );
//var storedfilelist = cmd2.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM ""FilesetEntry"", ""FileLookup"" WHERE ""FilesetEntry"".""FilesetID"" = ? AND ""FileLookup"".""ID"" = ""FilesetEntry"".""FileID"" AND ""FileLookup"".""BlocksetID"" != ? AND ""FileLookup"".""BlocksetID"" != ?"), 0, filesetid, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID);
var storedlist = cmd2 . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FilesetEntry"".""FilesetID"" = ?" , 0 , filesetid );
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 )
filesetname = string . Format ( "version {0}: {1} (database id: {2})" , fileset . Item1 , fileset . Item3 , fileset . Item2 );
2023-12-12 18:25:14 +01:00
anyError . Add ( string . Format ( "Unexpected difference in fileset {0}, found {1} entries, but expected {2}" , filesetname , expandedlist , storedlist ));
2019-08-05 20:14:05 -04:00
}
2018-10-17 14:37:23 +02:00
}
2023-12-12 18:25:14 +01:00
if ( anyError . Any ())
{
throw new Interface . UserInformationException ( string . Join ( "\n\r" , anyError ), "FilesetDifferences" );
}
2016-04-04 18:11:48 +02:00
}
2013-04-21 20:00:37 +02:00
}
}
2019-08-05 20:14:05 -04:00
public interface IBlock
{
string Hash { get ; }
long Size { get ; }
}
internal class Block : IBlock
{
public string Hash { get ; private set ; }
public long Size { get ; private set ; }
public Block ( string hash , long size )
{
this . Hash = hash ;
this . Size = size ;
}
}
2013-04-27 15:13:14 +02:00
2016-02-18 16:46:53 +01:00
public IEnumerable < IBlock > GetBlocks ( long volumeid , System . Data . IDbTransaction transaction = null )
2019-08-05 20:14:05 -04:00
{
using ( var cmd = m_connection . CreateCommand ( transaction ))
using ( var rd = cmd . ExecuteReader ( @"SELECT DISTINCT ""Hash"", ""Size"" FROM ""Block"" WHERE ""VolumeID"" = ?" , volumeid ))
while ( rd . Read ())
2015-01-24 21:59:53 +01:00
yield return new Block ( rd . GetValue ( 0 ). ToString (), rd . GetInt64 ( 1 ));
2016-09-15 11:39:27 +02:00
}
2013-04-27 15:13:14 +02:00
private class BlocklistHashEnumerable : IEnumerable < string >
{
private class BlocklistHashEnumerator : IEnumerator < string >
{
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDataReader m_reader ;
private readonly BlocklistHashEnumerable m_parent ;
2013-04-27 15:13:14 +02:00
private string m_path = null ;
private bool m_first = true ;
private string m_current = null ;
public BlocklistHashEnumerator ( BlocklistHashEnumerable parent , System . Data . IDataReader reader )
{
m_reader = reader ;
m_parent = parent ;
}
2019-08-05 20:14:05 -04:00
public string Current { get { return m_current ; } }
2013-04-27 15:13:14 +02:00
public void Dispose ()
{
}
object System . Collections . IEnumerator . Current { get { return this . Current ; } }
public bool MoveNext ()
{
m_first = false ;
if ( m_path == null )
{
m_path = m_reader . GetValue ( 0 ). ToString ();
m_current = m_reader . GetValue ( 6 ). ToString ();
return true ;
}
else
{
if ( m_current == null )
return false ;
if (! m_reader . Read ())
{
m_current = null ;
m_parent . MoreData = false ;
return false ;
}
var np = m_reader . GetValue ( 0 ). ToString ();
if ( m_path != np )
{
m_current = null ;
return false ;
}
m_current = m_reader . GetValue ( 6 ). ToString ();
return true ;
}
}
public void Reset ()
{
if (! m_first )
throw new Exception ( "Iterator reset not supported" );
m_first = false ;
}
}
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDataReader m_reader ;
2013-04-27 15:13:14 +02:00
public BlocklistHashEnumerable ( System . Data . IDataReader reader )
{
m_reader = reader ;
this . MoreData = true ;
}
public bool MoreData { get ; protected set ; }
public IEnumerator < string > GetEnumerator ()
{
return new BlocklistHashEnumerator ( this , m_reader );
}
System . Collections . IEnumerator System . Collections . IEnumerable . GetEnumerator ()
{
return this . GetEnumerator ();
}
}
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
""D"".""FilesetID"" = ? AND
(""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
AND (""B"".""BlocksetID"" = ? OR ""B"".""BlocksetID"" = ?)
AND ""A"".""FilesetID"" = ?
) G
LEFT OUTER JOIN
""BlocklistHash"" H
ON
""H"".""BlocksetID"" = ""G"".""MetaBlocksetID""
ORDER BY
""G"".""Path"", ""H"".""Index""
" ;
2016-02-25 19:15:51 +01:00
public void WriteFileset ( Volumes . FilesetVolumeWriter filesetvolume , long filesetId , System . Data . IDbTransaction transaction )
2013-04-27 15:13:14 +02:00
{
using ( var cmd = m_connection . CreateCommand ())
{
2019-08-05 20:14:05 -04:00
cmd . Transaction = transaction ;
2016-04-04 18:11:48 +02:00
cmd . CommandText = LIST_FOLDERS_AND_SYMLINKS ;
2013-04-27 15:13:14 +02:00
cmd . AddParameter ( FOLDER_BLOCKSET_ID );
cmd . AddParameter ( SYMLINK_BLOCKSET_ID );
cmd . AddParameter ( filesetId );
2016-04-04 18:11:48 +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 );
var path = rd . GetValue ( 2 ). ToString ();
var metalength = rd . ConvertValueToInt64 ( 3 , - 1 );
var metahash = rd . GetValue ( 4 ). ToString ();
var metablockhash = rd . GetValue ( 6 ). ToString ();
var metablocklisthash = rd . GetValue ( 7 ). ToString ();
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
2016-04-04 18:11:48 +02:00
cmd . CommandText = LIST_FILESETS ;
2013-04-27 15:13:14 +02:00
cmd . Parameters . Clear ();
cmd . AddParameter ( filesetId );
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
{
var path = rd . GetValue ( 0 ). ToString ();
var filehash = rd . GetValue ( 3 ). ToString ();
var size = rd . ConvertValueToInt64 ( 2 );
var lastmodified = new DateTime ( rd . ConvertValueToInt64 ( 1 , 0 ), DateTimeKind . Utc );
var metahash = rd . GetValue ( 4 ). ToString ();
var metasize = rd . ConvertValueToInt64 ( 5 , - 1 );
var p = rd . GetValue ( 6 );
var blrd = ( p == null || p == DBNull . Value ) ? null : new BlocklistHashEnumerable ( rd );
var blockhash = rd . GetValue ( 7 ). ToString ();
var blocksize = rd . ConvertValueToInt64 ( 8 , - 1 );
var metablockhash = rd . GetValue ( 9 ). ToString ();
//var metablocksize = rd.ConvertValueToInt64(10, -1);
var metablocklisthash = rd . GetValue ( 11 ). ToString ();
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
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 ; }
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDbConnection m_connection ;
2013-05-13 22:32:05 +02:00
public FilteredFilenameTable ( System . Data . IDbConnection connection , Library . Utility . IFilter filter , System . Data . IDbTransaction transaction )
2013-05-11 12:03:15 +02:00
{
m_connection = connection ;
Tablename = "Filenames-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2013-05-13 22:32:05 +02:00
var type = Library . Utility . 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
if ( type != Library . Utility . FilterType . Regexp && ! Library . Utility . Utility . IsFSCaseSensitive && filter . ToString (). Any ( x => x > 127 ))
type = Library . Utility . FilterType . Regexp ;
2019-08-05 20:14:05 -04:00
2018-03-21 14:53:45 -06:00
if ( type == Library . Utility . FilterType . Regexp || type == Library . Utility . FilterType . Group )
2013-05-11 12:03:15 +02:00
{
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
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
2013-05-11 12:03:15 +02:00
cmd . Transaction = transaction ;
cmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMPORARY TABLE ""{0}"" (""Path"" TEXT NOT NULL)" , Tablename ));
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2013-05-11 12:03:15 +02:00
{
cmd . CommandText = string . Format ( @"INSERT INTO ""{0}"" (""Path"") VALUES (?)" , Tablename );
cmd . AddParameter ();
cmd . Transaction = tr . Parent ;
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
{
var p = rd . GetValue ( 0 ). ToString ();
2019-08-05 20:14:05 -04:00
if ( Library . Utility . FilterExpression . Matches ( filter , p ))
2013-05-11 12:03:15 +02:00
{
cmd . SetParameterValue ( 0 , p );
cmd . ExecuteNonQuery ();
}
}
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 ();
var args = new List < object >();
2019-08-05 20:14:05 -04:00
foreach ( var f in (( Library . Utility . FilterExpression ) filter ). GetSimpleList ())
2013-05-11 12:03:15 +02:00
{
2020-09-01 21:45:27 -07:00
if ( type == FilterType . Wildcard )
2013-05-11 12:03:15 +02:00
{
sb . Append ( @"""Path"" LIKE ? OR " );
args . Add ( f . Replace ( '*' , '%' ). Replace ( '?' , '_' ));
}
else
{
2013-05-11 13:04:01 +02:00
sb . Append ( @"""Path"" = ? OR " );
2013-05-11 12:03:15 +02:00
args . Add ( f );
}
}
2019-08-05 20:14:05 -04:00
2013-05-11 13:04:01 +02:00
sb . Length = sb . Length - " OR " . Length ;
2019-08-05 20:14:05 -04:00
using ( var cmd = m_connection . CreateCommand ())
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
2013-11-20 19:16:35 +01:00
{
cmd . Transaction = tr . Parent ;
cmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMPORARY TABLE ""{0}"" (""Path"" TEXT NOT NULL)" , Tablename ));
2017-11-26 15:24:53 -08:00
cmd . ExecuteNonQuery ( string . Format ( @"INSERT INTO ""{0}"" SELECT DISTINCT ""Path"" FROM ""File"" WHERE " + sb , Tablename ), args . ToArray ());
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 ())
2013-08-23 23:36:25 +02:00
cmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , Tablename ));
2013-05-11 12:03:15 +02:00
}
2013-08-23 23:36:25 +02:00
catch { }
2013-05-11 12:03:15 +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
2013-07-23 18:56:01 +02:00
public void RenameRemoteFile ( string oldname , string newname , System . Data . IDbTransaction transaction )
{
2019-08-05 20:14:05 -04:00
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
using ( var cmd = m_connection . CreateCommand ())
2013-07-23 18:56:01 +02:00
{
cmd . Transaction = tr . Parent ;
2019-08-05 20:14:05 -04:00
2013-07-23 18:56:01 +02:00
//Rename the old entry, to preserve ID links
var c = cmd . ExecuteNonQuery ( @"UPDATE ""Remotevolume"" SET ""Name"" = ? WHERE ""Name"" = ?" , newname , oldname );
if ( c != 1 )
throw new Exception ( string . Format ( "Unexpected result from renaming \"{0}\" to \"{1}\", expected {2} got {3}" , oldname , newname , 1 , c ));
2019-08-05 20:14:05 -04:00
2013-07-23 18:56:01 +02:00
// Grab the type of entry
var type = ( RemoteVolumeType ) Enum . Parse ( typeof ( RemoteVolumeType ), cmd . ExecuteScalar ( @"SELECT ""Type"" FROM ""Remotevolume"" WHERE ""Name"" = ?" , newname ). ToString (), 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>
public virtual long CreateFileset ( long volumeid , DateTime timestamp , System . Data . IDbTransaction transaction = null )
{
using ( var cmd = m_connection . CreateCommand ())
using ( var tr = new TemporaryTransactionWrapper ( m_connection , transaction ))
{
2019-09-07 15:02:10 -04:00
cmd . Transaction = tr . Parent ;
var id = cmd . ExecuteScalarInt64 ( @"INSERT INTO ""Fileset"" (""OperationID"", ""Timestamp"", ""VolumeID"", ""IsFullBackup"") VALUES (?, ?, ?, ?); SELECT last_insert_rowid();" , - 1 , m_operationid , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( timestamp ), volumeid , BackupType . PARTIAL_BACKUP );
2013-07-22 16:54:19 +02:00
tr . Commit ();
return id ;
}
}
2019-08-30 11:15:26 -04:00
2013-07-22 16:54:19 +02:00
public void AddIndexBlockLink ( long indexVolumeID , long blockVolumeID , System . Data . IDbTransaction transaction )
{
m_insertIndexBlockLink . Transaction = transaction ;
m_insertIndexBlockLink . SetParameterValue ( 0 , indexVolumeID );
m_insertIndexBlockLink . SetParameterValue ( 1 , blockVolumeID );
m_insertIndexBlockLink . ExecuteNonQuery ();
}
2014-12-30 18:26:08 +01:00
2016-02-22 21:27:12 +01:00
public IEnumerable < Tuple < string , byte [], int >> GetBlocklists ( long volumeid , long blocksize , int hashsize , System . Data . 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
{
2019-08-05 20:14:05 -04:00
var sql = string . Format ( @"SELECT ""A"".""Hash"", ""C"".""Hash"" FROM " +
@"(SELECT ""BlocklistHash"".""BlocksetID"", ""Block"".""Hash"", * FROM ""BlocklistHash"",""Block"" WHERE ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""Block"".""VolumeID"" = ?) A, " +
@" ""BlocksetEntry"" B, ""Block"" C WHERE ""B"".""BlocksetID"" = ""A"".""BlocksetID"" AND " +
@" ""B"".""Index"" >= (""A"".""Index"" * {0}) AND ""B"".""Index"" < ((""A"".""Index"" + 1) * {0}) AND ""C"".""ID"" = ""B"".""BlockID"" " +
2014-12-30 18:26:08 +01:00
@" ORDER BY ""A"".""BlocksetID"", ""B"".""Index""" ,
2016-03-26 13:14:05 +01:00
blocksize / hashsize
2014-12-30 18:26:08 +01:00
);
string curHash = null ;
int index = 0 ;
byte [] buffer = new byte [ blocksize ];
2019-08-05 20:14:05 -04:00
using ( var rd = cmd . ExecuteReader ( sql , volumeid ))
2014-12-30 18:26:08 +01:00
while ( rd . Read ())
{
var blockhash = rd . GetValue ( 0 ). ToString ();
2016-02-23 20:56:24 +01:00
if (( blockhash != curHash && curHash != null ) || index + hashsize > buffer . Length )
2014-12-30 18:26:08 +01:00
{
yield return new Tuple < string , byte [], int >( curHash , buffer , index );
2018-06-06 08:37:09 +02:00
buffer = new byte [ blocksize ];
2014-12-30 18:26:08 +01:00
index = 0 ;
}
var hash = Convert . FromBase64String ( rd . GetValue ( 1 ). ToString ());
Array . Copy ( hash , 0 , buffer , index , hashsize );
curHash = blockhash ;
index += hashsize ;
}
2016-02-23 20:56:24 +01:00
if ( curHash != null )
2014-12-30 18:26:08 +01:00
yield return new Tuple < string , byte [], int >( curHash , buffer , index );
}
}
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>
2019-09-10 12:53:37 -04: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 ))
{
using ( var cmd = m_connection . CreateCommand ())
{
cmd . Transaction = tr . Parent ;
2019-09-10 12:53:37 -04:00
cmd . ExecuteNonQuery ( @"UPDATE ""Fileset"" SET ""IsFullBackup"" = ? WHERE ""ID"" = ?;" , isFullBackup , fileSetId );
2019-09-01 13:12:03 -04:00
}
tr . Commit ();
}
}
2015-02-15 23:26:52 +01:00
public void PurgeLogData ( DateTime threshold )
{
2019-08-05 20:14:05 -04:00
using ( var tr = m_connection . BeginTransaction ())
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 );
2015-02-15 23:26:52 +01:00
cmd . ExecuteNonQuery ( @"DELETE FROM ""LogData"" WHERE ""Timestamp"" < ?" , t );
cmd . ExecuteNonQuery ( @"DELETE FROM ""RemoteOperation"" WHERE ""Timestamp"" < ?" , t );
tr . Commit ();
}
}
2019-08-05 20:14:05 -04:00
2019-09-07 17:16:34 -04:00
public void PurgeDeletedVolumes ( DateTime threshold )
{
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
{
m_removedeletedremotevolumeCommand . Transaction = tr ;
m_removedeletedremotevolumeCommand . SetParameterValue ( 0 , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( threshold ));
m_removedeletedremotevolumeCommand . ExecuteNonQuery ();
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
DisposeAllFields < System . Data . IDbCommand >( this , false );
if ( ShouldCloseConnection && m_connection != null )
{
if ( m_connection . State == System . Data . ConnectionState . Open )
2019-04-22 21:11:27 -07:00
{
using ( IDbTransaction transaction = m_connection . BeginTransaction ())
{
using ( IDbCommand command = m_connection . CreateCommand ( transaction ))
{
// SQLite recommends that PRAGMA optimize is run just before closing each database connection.
command . ExecuteNonQuery ( "PRAGMA optimize" );
transaction . Commit ();
}
}
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
}
public void WriteResults ()
{
if ( IsDisposed )
return ;
2013-05-25 16:40:15 +02:00
if ( m_connection != null && m_result != null )
{
m_result . FlushLog ();
2016-12-01 23:59:54 +01:00
if ( m_result . EndTime . Ticks == 0 )
m_result . EndTime = DateTime . UtcNow ;
2018-11-14 08:47:01 -02:00
var serializer = new JsonFormatSerializer ();
LogMessage ( "Result" ,
serializer . SerializeResults ( m_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>
public long GetOrCreatePathPrefix ( string prefix , System . Data . IDbTransaction transaction )
{
// 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 ;
m_findpathprefixCommand . SetParameterValue ( 0 , prefix );
var id = m_findpathprefixCommand . ExecuteScalarInt64 ();
if ( id < 0 )
{
m_insertpathprefixCommand . Transaction = transaction ;
m_insertpathprefixCommand . SetParameterValue ( 0 , prefix );
id = m_insertpathprefixCommand . ExecuteScalarInt64 ();
}
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 [] {
System . IO . Path . DirectorySeparatorChar ,
System . IO . Path . AltDirectorySeparatorChar ,
};
/// <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
}
}