2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-06-05 11:02:56 +02:00
// 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.
2022-12-30 10:59:29 -08:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using Duplicati.Server.Serialization.Interface ;
using System.Text ;
using Duplicati.Library.RestAPI ;
2024-08-19 13:51:14 -03:00
using Duplicati.Library.Encryption ;
2024-08-20 17:08:32 +02:00
using Duplicati.Library.DynamicLoader ;
using Duplicati.Library.Main ;
2024-11-25 17:27:50 +01:00
using Duplicati.Library.AutoUpdater ;
2025-01-14 14:03:48 +01:00
using System.Data ;
using Duplicati.Library.Main.Database ;
2025-04-03 13:59:07 +02:00
using System.Globalization ;
2025-01-14 14:03:48 +01:00
#nullable enable
2022-12-30 10:59:29 -08:00
namespace Duplicati.Server.Database
{
public class Connection : IDisposable
{
2025-03-03 17:39:25 +01:00
private readonly IDbConnection m_connection ;
private readonly IDbCommand m_errorcmd ;
2022-12-30 10:59:29 -08:00
public readonly object m_lock = new object ();
public const int ANY_BACKUP_ID = - 1 ;
public const int SERVER_SETTINGS_ID = - 2 ;
private readonly Dictionary < string , Backup > m_temporaryBackups = new Dictionary < string , Backup >();
2024-08-20 17:10:34 +02:00
private readonly bool m_encryptSensitiveFields ;
2024-10-24 15:56:39 +02:00
private readonly EncryptedFieldHelper . KeyInstance ? m_key ;
2024-08-20 21:10:54 +02:00
private static readonly HashSet < string > _encryptedFields =
2024-08-20 17:08:32 +02:00
BackendLoader . Backends . SelectMany ( x => x . SupportedCommands ?? [])
. Concat ( EncryptionLoader . Modules . SelectMany ( x => x . SupportedCommands ?? []))
. Concat ( CompressionLoader . Modules . SelectMany ( x => x . SupportedCommands ?? []))
. Concat ( GenericLoader . Modules . SelectMany ( x => x . SupportedCommands ?? []))
. Concat ( WebLoader . Modules . SelectMany ( x => x . SupportedCommands ?? []))
. Concat ( new Options ( new Dictionary < string , string >()). SupportedCommands )
2024-09-18 11:07:59 +02:00
. Where ( x => x . Type == Library . Interface . CommandLineArgument . ArgumentType . Password )
2024-08-20 17:08:32 +02:00
. SelectMany ( x => new string [] { x . Name }. Concat ( x . Aliases ?? []))
. SelectMany ( x => new string [] { x , $"--{x}" })
. Concat ([
2024-08-20 21:11:31 +02:00
ServerSettings . CONST . JWT_CONFIG ,
2024-09-18 11:07:59 +02:00
ServerSettings . CONST . PBKDF_CONFIG ,
2024-10-29 11:15:06 +01:00
ServerSettings . CONST . REMOTE_CONTROL_CONFIG ,
ServerSettings . CONST . SERVER_SSL_CERTIFICATE ,
ServerSettings . CONST . SERVER_SSL_CERTIFICATEPASSWORD
2024-08-20 17:08:32 +02:00
])
. ToHashSet ( StringComparer . OrdinalIgnoreCase );
2025-03-03 17:39:25 +01:00
public Connection ( IDbConnection connection , bool disableFieldEncryption , EncryptedFieldHelper . KeyInstance ? key )
2022-12-30 10:59:29 -08:00
{
2024-08-20 17:10:34 +02:00
m_encryptSensitiveFields = ! disableFieldEncryption ;
2024-10-24 15:56:39 +02:00
m_key = key ;
2022-12-30 10:59:29 -08:00
m_connection = connection ;
2025-04-03 13:59:07 +02:00
m_errorcmd = m_connection . CreateCommand ( @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (@BackupId,@Message,@Exception,@Timestamp)" );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
this . ApplicationSettings = new ServerSettings ( this );
}
2024-06-07 15:56:43 +02:00
2025-01-27 08:52:06 +01:00
public bool IsEncryptingFields => m_encryptSensitiveFields ;
2024-08-20 17:10:34 +02:00
public void ReWriteAllFieldsIfEncryptionChanged ()
{
// The token is automatically decrypted when the settings are loaded
// In case the password has changed, this will fail and return the encrypted
// hex-string, but will crash before reaching this point
2024-08-20 21:10:15 +02:00
if ( this . ApplicationSettings . EncryptedFields != m_encryptSensitiveFields )
2024-08-20 17:10:34 +02:00
{
var backups = this . Backups ;
foreach ( var b in backups )
{
(( Backup ) b ). LoadChildren ( this );
AddOrUpdateBackup ( b , false , null );
}
this . SetSettings ( this . GetSettings ( ANY_BACKUP_ID ), ANY_BACKUP_ID );
2024-08-20 21:10:15 +02:00
this . ApplicationSettings . EncryptedFields = m_encryptSensitiveFields ;
2024-08-20 17:10:34 +02:00
}
}
2024-08-28 00:18:55 +02:00
public void SetPreloadSettingsIfChanged ( Dictionary < string , string > newsettings )
{
if ( newsettings == null || newsettings . Count == 0 )
return ;
var settingsHash = Convert . ToBase64String ( System . Security . Cryptography . SHA256 . HashData ( System . Text . Encoding . UTF8 . GetBytes ( System . Text . Json . JsonSerializer . Serialize ( newsettings . OrderBy ( x => x . Key )))));
if ( settingsHash == this . ApplicationSettings . PreloadSettingsHash )
return ;
newsettings = newsettings
. ToDictionary ( x => x . Key . StartsWith ( "--" ) ? x . Key : $"--{x.Key}" , x => x . Value );
var currentSettings = this . Settings ;
var filters = currentSettings . Where ( x => x . Filter != null ). ToDictionary ( x => x . Name , x => x . Filter );
var updatedSettings = currentSettings
. Where ( x => ! newsettings . ContainsKey ( x . Name ))
. Concat ( newsettings . Where ( x => x . Value != null ). Select ( x => new Setting
{
Name = x . Key ,
Value = x . Value ,
Filter = filters . GetValueOrDefault ( x . Key ) ?? ""
}));
this . Settings = updatedSettings . ToArray ();
this . ApplicationSettings . PreloadSettingsHash = settingsHash ;
}
2022-12-30 10:59:29 -08:00
public void LogError ( string backupid , string message , Exception ex )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
if (! long . TryParse ( backupid , out long id ))
id = - 1 ;
2025-01-14 14:03:48 +01:00
2025-04-03 13:59:07 +02:00
m_errorcmd . SetParameterValue ( "@BackupId" , id )
. SetParameterValue ( "@Message" , message )
. SetParameterValue ( "@Exception" , ex ?. ToString ())
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( DateTime . UtcNow ))
. ExecuteNonQuery ();
2022-12-30 10:59:29 -08:00
}
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
public void ExecuteWithCommand ( Action < IDbCommand > f )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
using ( var cmd = m_connection . CreateCommand ())
2022-12-30 10:59:29 -08:00
f ( cmd );
}
2024-06-07 15:56:43 +02:00
public Serializable . ImportExportStructure PrepareBackupForExport ( IBackup backup )
2022-12-30 10:59:29 -08:00
{
var scheduleId = GetScheduleIDsFromTags ( new string [] { "ID=" + backup . ID });
2024-06-07 15:56:43 +02:00
return new Serializable . ImportExportStructure ()
{
2025-01-14 14:03:48 +01:00
CreatedByVersion = UpdaterManager . SelfVersion . Version ?? "Unknown" ,
2024-06-07 15:56:43 +02:00
Backup = ( Database . Backup ) backup ,
2025-01-14 14:03:48 +01:00
Schedule = scheduleId != null && scheduleId . Any () ? ( Schedule ?) GetSchedule ( scheduleId . First ()) : null ,
2024-06-07 15:56:43 +02:00
DisplayNames = SpecialFolders . GetSourceNames ( backup )
};
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public string RegisterTemporaryBackup ( IBackup backup )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
if ( backup == null )
throw new ArgumentNullException ( nameof ( backup ));
if ( backup . ID != null )
throw new ArgumentException ( "Backup is already active, cannot make temporary" );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
backup . ID = Guid . NewGuid (). ToString ( "D" );
m_temporaryBackups . Add ( backup . ID , ( Backup ) backup );
return backup . ID ;
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public void UnregisterTemporaryBackup ( IBackup backup )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
m_temporaryBackups . Remove ( backup . ID );
}
public void UpdateTemporaryBackup ( IBackup backup )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
if ( m_temporaryBackups . Remove ( backup . ID ))
m_temporaryBackups . Add ( backup . ID , ( Backup ) backup );
}
2025-01-14 14:03:48 +01:00
public IBackup ? GetTemporaryBackup ( string id )
2022-12-30 10:59:29 -08:00
{
if ( string . IsNullOrEmpty ( id ))
return null ;
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
2025-01-14 14:03:48 +01:00
m_temporaryBackups . TryGetValue ( id , out var b );
2022-12-30 10:59:29 -08:00
return b ;
}
}
public ServerSettings ApplicationSettings { get ; private set ; }
2024-06-07 15:56:43 +02:00
2025-01-14 14:03:48 +01:00
internal IDictionary < string , string? > GetMetadata ( long id )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
2025-01-14 14:03:48 +01:00
( rd ) => new KeyValuePair < string , string? >(
ConvertToString ( rd , 0 ) ?? "" ,
2022-12-30 10:59:29 -08:00
ConvertToString ( rd , 1 )
),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id )
)
. ToDictionary (( k ) => k . Key , ( k ) => k . Value );
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
internal void SetMetadata ( IDictionary < string , string > values , long id , IDbTransaction ? transaction )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ),
2022-12-30 10:59:29 -08:00
values ?? new Dictionary < string , string >(),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (@BackupId, @Name, @Value)" ),
( cmd , f ) => cmd . SetParameterValue ( "@BackupId" , id )
. SetParameterValue ( "@Name" , f . Key )
. SetParameterValue ( "@Value" , f . Value )
2022-12-30 10:59:29 -08:00
);
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( tr != null )
tr . Commit ();
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
internal IFilter [] GetFilters ( long id )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => ( IFilter ) new Filter ()
{
Order = ConvertToInt64 ( rd , 0 ),
2022-12-30 10:59:29 -08:00
Include = ConvertToBoolean ( rd , 1 ),
Expression = ConvertToString ( rd , 2 ) ?? ""
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = @Id ORDER BY ""Order"" " )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. ToArray ();
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
internal void SetFilters ( IEnumerable < IFilter > values , long id , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""Filter"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ),
2022-12-30 10:59:29 -08:00
values ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (@Id, @Order, @Include, @Expression)" ),
( cmd , f ) => cmd . SetParameterValue ( "@Id" , id )
. SetParameterValue ( "@Order" , f . Order )
. SetParameterValue ( "@Include" , f . Include )
. SetParameterValue ( "@Expression" , f . Expression )
2022-12-30 10:59:29 -08:00
);
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( tr != null )
tr . Commit ();
}
}
public ISetting [] GetSettings ( long id )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => ( ISetting ) new Setting ()
{
2022-12-30 10:59:29 -08:00
Filter = ConvertToString ( rd , 0 ) ?? "" ,
Name = ConvertToString ( rd , 1 ) ?? "" ,
2024-10-24 15:56:39 +02:00
Value = DecryptSensitiveFields ( ConvertToString ( rd , 2 ) ?? "" , m_key )
2022-12-30 10:59:29 -08:00
//TODO: Attach the argument information
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. ToArray ();
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
internal void SetSettings ( IEnumerable < ISetting > values , long id , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
2024-08-20 17:10:34 +02:00
if ( m_encryptSensitiveFields )
2024-08-20 17:08:32 +02:00
values = values . Select ( x => new Setting
{
Filter = x . Filter ,
Name = x . Name ,
2024-10-24 15:56:39 +02:00
Value = EncryptSensitiveFields ( x . Name , x . Value , m_key )
2024-08-20 17:08:32 +02:00
}). ToList ();
2024-08-15 10:17:01 -03:00
2022-12-30 10:59:29 -08:00
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""Option"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ),
2022-12-30 10:59:29 -08:00
values ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (@BackupId, @Filter, @Name, @Value)" ),
( cmd , f ) =>
2024-06-07 15:56:43 +02:00
{
if ( FIXMEGlobal . PASSWORD_PLACEHOLDER . Equals ( f . Value ))
2022-12-30 10:59:29 -08:00
throw new Exception ( "Attempted to save a property with the placeholder password" );
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( "@BackupId" , id )
. SetParameterValue ( "@Filter" , f . Filter ?? "" )
. SetParameterValue ( "@Name" , f . Name ?? "" )
. SetParameterValue ( "@Value" , f . Value ?? "" );
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
);
2022-12-30 10:59:29 -08:00
if ( tr != null )
tr . Commit ();
}
}
2024-06-07 15:56:43 +02:00
2025-01-14 14:03:48 +01:00
internal string? [] GetSources ( long id )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
( rd ) => ConvertToString ( rd , 0 ),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. ToArray ();
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
internal void SetSources ( IEnumerable < string > values , long id , IDbTransaction transaction )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""Source"" WHERE ""BackupID"" = @Id" )
. SetParameterValue ( "@Id" , id ),
2022-12-30 10:59:29 -08:00
values ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (@BackupId, @Path)" ),
( cmd , f ) => cmd . SetParameterValue ( "@BackupId" , id )
. SetParameterValue ( "@Path" , f )
2024-06-07 15:56:43 +02:00
);
2022-12-30 10:59:29 -08:00
if ( tr != null )
tr . Commit ();
}
}
internal long [] GetBackupIDsForTags ( string [] tags )
{
if ( tags == null || tags . Length == 0 )
return new long [ 0 ];
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( tags . Length == 1 && tags [ 0 ]. StartsWith ( "ID=" , StringComparison . Ordinal ))
return new long [] { long . Parse ( tags [ 0 ]. Substring ( "ID=" . Length )) };
2024-06-07 15:56:43 +02:00
lock ( m_lock )
using ( var cmd = m_connection . CreateCommand ())
2022-12-30 10:59:29 -08:00
{
var sb = new StringBuilder ();
2024-06-07 15:56:43 +02:00
foreach ( var t in tags )
2022-12-30 10:59:29 -08:00
{
if ( sb . Length != 0 )
sb . Append ( " OR " );
2024-04-15 08:24:01 +02:00
sb . Append ( @" (',' || ""Tags"" || ',' LIKE '%,' || ? || ',%') " );
2022-12-30 10:59:29 -08:00
var p = cmd . CreateParameter ();
p . Value = t ;
cmd . Parameters . Add ( p );
}
2025-04-03 16:38:51 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
return Read ( cmd , ( rd ) => ConvertToInt64 ( rd , 0 )). ToArray ();
}
}
2025-01-14 14:03:48 +01:00
public IBackup ? GetBackup ( string id )
2022-12-30 10:59:29 -08:00
{
if ( string . IsNullOrWhiteSpace ( id ))
throw new ArgumentNullException ( nameof ( id ));
return long . TryParse ( id , out long lid ) ? GetBackup ( lid ) : GetTemporaryBackup ( id );
}
2025-01-14 14:03:48 +01:00
internal IBackup ? GetBackup ( long id )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
var bk = ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => new Backup
{
2022-12-30 10:59:29 -08:00
ID = ConvertToInt64 ( rd , 0 ). ToString (),
Name = ConvertToString ( rd , 1 ),
Description = ConvertToString ( rd , 2 ),
Tags = ( ConvertToString ( rd , 3 ) ?? "" ). Split ( new char [] { ',' }, StringSplitOptions . RemoveEmptyEntries ),
2024-10-24 15:56:39 +02:00
TargetURL = EncryptedFieldHelper . Decrypt ( ConvertToString ( rd , 4 ), m_key ),
2022-12-30 10:59:29 -08:00
DBPath = ConvertToString ( rd , 5 ),
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = @Id" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. FirstOrDefault ();
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( bk != null )
bk . LoadChildren ( this );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
return bk ;
}
}
2024-06-07 15:56:43 +02:00
2025-01-14 14:03:48 +01:00
public ISchedule ? GetSchedule ( long id )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
var bk = ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => new Schedule
{
2022-12-30 10:59:29 -08:00
ID = ConvertToInt64 ( rd , 0 ),
Tags = ( ConvertToString ( rd , 1 ) ?? "" ). Split ( new char [] { ',' }, StringSplitOptions . RemoveEmptyEntries ),
Time = ConvertToDateTime ( rd , 2 ),
Repeat = ConvertToString ( rd , 3 ),
LastRun = ConvertToDateTime ( rd , 4 ),
Rule = ConvertToString ( rd , 5 ),
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = @Id" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. FirstOrDefault ();
return bk ;
}
}
2024-06-07 15:56:43 +02:00
public bool IsUnencryptedOrPassphraseStored ( long id )
2022-12-30 10:59:29 -08:00
{
lock ( m_lock )
{
var usesEncryption = ReadFromDb (
( rd ) => ConvertToBoolean ( rd , 0 ),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT VALUE != '' FROM ""Option"" WHERE BackupID = @Id AND NAME='encryption-module'" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. FirstOrDefault ();
if (! usesEncryption )
{
return true ;
}
return ReadFromDb (
( rd ) => ConvertToBoolean ( rd , 0 ),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT VALUE != '' FROM ""Option"" WHERE BackupID = @Id AND NAME='passphrase'" )
. SetParameterValue ( "@Id" , id ))
2022-12-30 10:59:29 -08:00
. FirstOrDefault ();
}
}
2024-06-07 15:56:43 +02:00
public long [] GetScheduleIDsFromTags ( string [] tags )
2022-12-30 10:59:29 -08:00
{
if ( tags == null || tags . Length == 0 )
return new long [ 0 ];
2024-06-07 15:56:43 +02:00
lock ( m_lock )
using ( var cmd = m_connection . CreateCommand ())
2022-12-30 10:59:29 -08:00
{
var sb = new StringBuilder ();
2024-06-07 15:56:43 +02:00
2025-04-03 17:05:13 +02:00
foreach (( var t , var i ) in tags . Select (( t , i ) => ( t , i )))
2022-12-30 10:59:29 -08:00
{
if ( sb . Length != 0 )
sb . Append ( " OR " );
2025-04-03 17:05:13 +02:00
sb . Append ( @ $" (',' || ""Tags"" || ',' LIKE '%,' || @p{i} || ',%') " );
cmd . AddNamedParameter ( $"@p{i}" , t );
2022-12-30 10:59:29 -08:00
}
2025-04-03 16:38:51 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
return Read ( cmd , ( rd ) => ConvertToInt64 ( rd , 0 )). ToArray ();
}
}
2025-01-14 14:03:48 +01:00
public void AddOrUpdateBackupAndSchedule ( IBackup item , ISchedule ? schedule )
2022-12-30 10:59:29 -08:00
{
AddOrUpdateBackup ( item , true , schedule );
}
2025-01-14 14:03:48 +01:00
public string? ValidateBackup ( IBackup item , ISchedule ? schedule )
2022-12-30 10:59:29 -08:00
{
if ( string . IsNullOrWhiteSpace ( item . Name ))
return "Missing a name" ;
if ( string . IsNullOrWhiteSpace ( item . TargetURL ))
return "Missing a target" ;
if ( item . Sources == null || item . Sources . Any ( x => string . IsNullOrWhiteSpace ( x )) || item . Sources . Length == 0 )
return "Invalid source list" ;
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
var disabled_encryption = false ;
var passphrase = string . Empty ;
var gpgAsymmetricEncryption = false ;
if ( item . Settings != null )
{
foreach ( var s in item . Settings )
if ( string . Equals ( s . Name , "--no-encryption" , StringComparison . OrdinalIgnoreCase ))
disabled_encryption = string . IsNullOrWhiteSpace ( s . Value ) || Library . Utility . Utility . ParseBool ( s . Value , false );
else if ( string . Equals ( s . Name , "passphrase" , StringComparison . OrdinalIgnoreCase ))
passphrase = s . Value ;
else if ( string . Equals ( s . Name , "keep-versions" , StringComparison . OrdinalIgnoreCase ))
{
int i ;
if (! int . TryParse ( s . Value , out i ) || i <= 0 )
return "Retention value must be a positive integer" ;
}
else if ( string . Equals ( s . Name , "keep-time" , StringComparison . OrdinalIgnoreCase ))
{
try
{
var ts = Library . Utility . Timeparser . ParseTimeSpan ( s . Value );
if ( ts <= TimeSpan . FromMinutes ( 5 ))
return "Retention value must be more than 5 minutes" ;
}
catch
{
return "Retention value must be a valid timespan" ;
}
}
else if ( string . Equals ( s . Name , "dblock-size" , StringComparison . OrdinalIgnoreCase ))
{
try
{
var ds = Library . Utility . Sizeparser . ParseSize ( s . Value );
if ( ds < 1024 * 1024 )
return "DBlock size must be at least 1MB" ;
}
catch
{
return "DBlock value must be a valid size string" ;
}
}
else if ( string . Equals ( s . Name , "--blocksize" , StringComparison . OrdinalIgnoreCase ))
{
try
{
var ds = Library . Utility . Sizeparser . ParseSize ( s . Value );
if ( ds < 1024 || ds > int . MaxValue )
return "The blocksize must be at least 1KB" ;
}
catch
{
return "The blocksize value must be a valid size string" ;
}
}
else if ( string . Equals ( s . Name , "--prefix" , StringComparison . OrdinalIgnoreCase ))
{
if (! string . IsNullOrWhiteSpace ( s . Value ) && s . Value . Contains ( "-" ))
return "The prefix cannot contain hyphens (-)" ;
}
2024-06-07 15:56:43 +02:00
else if ( string . Equals ( s . Name , "--gpg-encryption-command" , StringComparison . OrdinalIgnoreCase ))
{
2022-12-30 10:59:29 -08:00
gpgAsymmetricEncryption = string . Equals ( s . Value , "--encrypt" , StringComparison . OrdinalIgnoreCase );
}
}
if (! disabled_encryption && ! gpgAsymmetricEncryption && string . IsNullOrWhiteSpace ( passphrase ))
return "Missing passphrase" ;
if ( schedule != null )
{
try
{
var ts = Library . Utility . Timeparser . ParseTimeSpan ( schedule . Repeat );
if ( ts <= TimeSpan . FromMinutes ( 5 ))
return "Schedule repetition time must be more than 5 minutes" ;
}
catch
{
return "Schedule repetition value must be a valid timespan" ;
}
}
return null ;
}
2024-06-07 15:56:43 +02:00
public void UpdateBackupDBPath ( IBackup item , string path )
2022-12-30 10:59:29 -08:00
{
lock ( m_lock )
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ( tr , @"UPDATE ""Backup"" SET ""DBPath""= @Dbpath WHERE ""ID""= @Id" ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( "@Dbpath" , path )
. SetParameterValue ( "@Id" , item . ID )
. ExecuteNonQuery ();
2022-12-30 10:59:29 -08:00
tr . Commit ();
}
}
}
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastDataUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
2025-01-14 14:03:48 +01:00
private void AddOrUpdateBackup ( IBackup item , bool updateSchedule , ISchedule ? schedule )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
bool update = item . ID != null ;
if (! update && item . DBPath == null )
{
var folder = FIXMEGlobal . DataFolder ;
if (! System . IO . Directory . Exists ( folder ))
System . IO . Directory . CreateDirectory ( folder );
2024-06-07 15:56:43 +02:00
for ( var i = 0 ; i < 100 ; i ++)
2022-12-30 10:59:29 -08:00
{
2025-01-27 22:51:45 +01:00
var guess = System . IO . Path . Combine ( folder , System . IO . Path . ChangeExtension ( CLIDatabaseLocator . GenerateRandomName (), ".sqlite" ));
2022-12-30 10:59:29 -08:00
if (! System . IO . File . Exists ( guess ))
{
(( Backup ) item ). DBPath = guess ;
break ;
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( item . DBPath == null )
throw new Exception ( "Unable to generate a unique database file name" );
}
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
null ,
2025-01-14 14:03:48 +01:00
[item] ,
2025-04-03 13:59:07 +02:00
cmd =>
{
if ( update )
cmd . SetCommandAndParameters ( @"UPDATE ""Backup"" SET ""Name""=@Name, ""Description""=@Description, ""Tags""=@Tags, ""TargetURL""=@TargetUrl WHERE ""ID""=@Id" );
else
cmd . SetCommandAndParameters ( @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (@Name,@Description,@Tags,@TargetUrl,@DbPath)" );
},
( cmd , n ) =>
2024-06-07 15:56:43 +02:00
{
if ( n . TargetURL . IndexOf ( FIXMEGlobal . PASSWORD_PLACEHOLDER , StringComparison . Ordinal ) >= 0 )
2022-12-30 10:59:29 -08:00
throw new Exception ( "Attempted to save a backup with the password placeholder" );
if ( update && long . Parse ( n . ID ) <= 0 )
throw new Exception ( "Invalid update, cannot update application settings through update method" );
2024-06-07 15:56:43 +02:00
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( "@Name" , n . Name )
. SetParameterValue ( "@Description" , n . Description ?? "" )
. SetParameterValue ( "@Tags" , string . Join ( "," , n . Tags ?? new string [ 0 ]))
. SetParameterValue ( "@TargetUrl" , m_encryptSensitiveFields ? EncryptedFieldHelper . Encrypt ( n . TargetURL , m_key ) : n . TargetURL );
if ( update )
cmd . SetParameterValue ( "@Id" , item . ID );
else
cmd . SetParameterValue ( "@DbPath" , n . DBPath );
2022-12-30 10:59:29 -08:00
});
if (! update )
2024-06-07 15:56:43 +02:00
using ( var cmd = m_connection . CreateCommand ())
2022-12-30 10:59:29 -08:00
{
cmd . Transaction = tr ;
2025-04-03 16:38:51 +02:00
item . ID = cmd . ExecuteScalarInt64 ( @"SELECT last_insert_rowid();" ). ToString ();
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
2025-01-14 14:03:48 +01:00
var id = long . Parse ( item . ID ?? "-1" );
if ( id <= 0 )
2022-12-30 10:59:29 -08:00
throw new Exception ( "Invalid addition, cannot update application settings through update method" );
SetSources ( item . Sources , id , tr );
SetSettings ( item . Settings , id , tr );
SetFilters ( item . Filters , id , tr );
SetMetadata ( item . Metadata , id , tr );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( updateSchedule )
{
2024-06-07 15:56:43 +02:00
var tags = new string [] { "ID=" + item . ID };
2022-12-30 10:59:29 -08:00
var existing = GetScheduleIDsFromTags ( tags );
if ( schedule == null && existing . Any ())
DeleteFromDb ( "Schedule" , existing . First (), tr );
else if ( schedule != null )
{
if ( existing . Any ())
{
var cur = GetSchedule ( existing . First ());
2025-01-14 14:03:48 +01:00
if ( cur != null )
{
cur . AllowedDays = schedule . AllowedDays ;
cur . Repeat = schedule . Repeat ;
cur . Tags = schedule . Tags ;
cur . Time = schedule . Time ;
schedule = cur ;
}
else
{
schedule . ID = - 1 ;
}
2022-12-30 10:59:29 -08:00
}
else
{
schedule . ID = - 1 ;
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
schedule . Tags = tags ;
AddOrUpdateSchedule ( schedule , tr );
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
tr . Commit ();
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastDataUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
internal void AddOrUpdateSchedule ( ISchedule item )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2022-12-30 10:59:29 -08:00
{
AddOrUpdateSchedule ( item , tr );
tr . Commit ();
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastDataUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
private void AddOrUpdateSchedule ( ISchedule item , IDbTransaction tr )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
bool update = item . ID >= 0 ;
OverwriteAndUpdateDb (
tr ,
null ,
2025-04-03 13:59:07 +02:00
[item] ,
cmd =>
{
if ( update )
cmd . SetCommandAndParameters ( @"UPDATE ""Schedule"" SET ""Tags""=@Tags, ""Time""=@Time, ""Repeat""=@Repeat, ""LastRun""=@LastRun, ""Rule""=@Rule WHERE ""ID""=@Id" );
else
cmd . SetCommandAndParameters ( @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (@Tags,@Time,@Repeat,@LastRun,@Rule)" );
},
( cmd , n ) =>
{
cmd . SetParameterValue ( "@Tags" , string . Join ( "," , n . Tags ?? new string [ 0 ]))
. SetParameterValue ( "@Time" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( n . Time ))
. SetParameterValue ( "@Repeat" , n . Repeat )
. SetParameterValue ( "@LastRun" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( n . LastRun ))
. SetParameterValue ( "@Rule" , n . Rule ?? "" );
if ( update )
cmd . SetParameterValue ( "@Id" , item . ID );
});
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if (! update )
2025-04-03 16:38:51 +02:00
using ( var cmd = m_connection . CreateCommand ( tr ))
item . ID = cmd . ExecuteScalarInt64 ( @"SELECT last_insert_rowid();" );
2022-12-30 10:59:29 -08:00
}
}
public void DeleteBackup ( long ID )
{
if ( ID < 0 )
return ;
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2022-12-30 10:59:29 -08:00
{
var existing = GetScheduleIDsFromTags ( new string [] { "ID=" + ID . ToString () });
if ( existing . Any ())
DeleteFromDb ( "Schedule" , existing . First (), tr );
DeleteFromDb ( "ErrorLog" , ID , "BackupID" , tr );
DeleteFromDb ( "Filter" , ID , "BackupID" , tr );
DeleteFromDb ( "Log" , ID , "BackupID" , tr );
DeleteFromDb ( "Metadata" , ID , "BackupID" , tr );
DeleteFromDb ( "Option" , ID , "BackupID" , tr );
DeleteFromDb ( "Source" , ID , "BackupID" , tr );
DeleteFromDb ( "Backup" , ID , tr );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
tr . Commit ();
}
}
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastDataUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public void DeleteBackup ( IBackup backup )
{
if ( backup . IsTemporary )
UnregisterTemporaryBackup ( backup );
else
DeleteBackup ( long . Parse ( backup . ID ));
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public void DeleteSchedule ( long ID )
{
if ( ID < 0 )
return ;
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
DeleteFromDb ( "Schedule" , ID );
2024-06-07 15:56:43 +02:00
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastDataUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public void DeleteSchedule ( ISchedule schedule )
{
DeleteSchedule ( schedule . ID );
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public IBackup [] Backups
{
get
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
var lst = ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => ( IBackup ) new Backup ()
{
2022-12-30 10:59:29 -08:00
ID = ConvertToInt64 ( rd , 0 ). ToString (),
Name = ConvertToString ( rd , 1 ),
Description = ConvertToString ( rd , 2 ),
Tags = ( ConvertToString ( rd , 3 ) ?? "" ). Split ( new char [] { ',' }, StringSplitOptions . RemoveEmptyEntries ),
2024-10-24 15:56:39 +02:00
TargetURL = EncryptedFieldHelper . Decrypt ( ConvertToString ( rd , 4 ), m_key ),
2022-12-30 10:59:29 -08:00
DBPath = ConvertToString ( rd , 5 ),
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" " ))
2022-12-30 10:59:29 -08:00
. ToArray ();
2024-06-07 15:56:43 +02:00
foreach ( var n in lst )
2022-12-30 10:59:29 -08:00
n . Metadata = GetMetadata ( long . Parse ( n . ID ));
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
return lst ;
}
}
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public ISchedule [] Schedules
{
get
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
2024-06-07 15:56:43 +02:00
( rd ) => ( ISchedule ) new Schedule ()
{
2022-12-30 10:59:29 -08:00
ID = ConvertToInt64 ( rd , 0 ),
Tags = ( ConvertToString ( rd , 1 ) ?? "" ). Split ( new char [] { ',' }, StringSplitOptions . RemoveEmptyEntries ),
Time = ConvertToDateTime ( rd , 2 ),
Repeat = ConvertToString ( rd , 3 ),
LastRun = ConvertToDateTime ( rd , 4 ),
Rule = ConvertToString ( rd , 5 ),
},
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" " ))
2022-12-30 10:59:29 -08:00
. ToArray ();
2024-06-07 15:56:43 +02:00
}
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public IFilter [] Filters
{
get { return GetFilters ( ANY_BACKUP_ID ); }
set { SetFilters ( value , ANY_BACKUP_ID ); }
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
public ISetting [] Settings
{
get { return GetSettings ( ANY_BACKUP_ID ); }
set { SetSettings ( value , ANY_BACKUP_ID ); }
}
public INotification [] GetNotifications ()
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb < Notification >( null ). Cast < INotification >(). ToArray ();
}
public bool DismissNotification ( long id )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
var notifications = GetNotifications ();
var cur = notifications . FirstOrDefault ( x => x . ID == id );
if ( cur == null )
return false ;
DeleteFromDb ( typeof ( Notification ). Name , id );
FIXMEGlobal . DataConnection . ApplicationSettings . UnackedError = notifications . Any ( x => x . ID != id && x . Type == Duplicati . Server . Serialization . NotificationType . Error );
FIXMEGlobal . DataConnection . ApplicationSettings . UnackedWarning = notifications . Any ( x => x . ID != id && x . Type == Duplicati . Server . Serialization . NotificationType . Warning );
}
2024-11-25 17:27:50 +01:00
// Guard against dismissing notifications before the provider is initialized
if ( FIXMEGlobal . Provider != null )
{
FIXMEGlobal . NotificationUpdateService . IncrementLastNotificationUpdateId ();
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
2022-12-30 10:59:29 -08:00
return true ;
}
public void RegisterNotification ( Serialization . NotificationType type , string title , string message , Exception ex , string backupid , string action , string logid , string messageid , string logtag , Func < INotification , INotification [], INotification > conflicthandler )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
{
var notification = new Notification ()
{
ID = - 1 ,
Type = type ,
Title = title ,
Message = message ,
Exception = ex == null ? "" : ex . ToString (),
BackupID = backupid ,
Action = action ?? "" ,
Timestamp = DateTime . UtcNow ,
LogEntryID = logid ,
MessageID = messageid ,
MessageLogTag = logtag
};
var conflictResult = conflicthandler ( notification , GetNotifications ());
if ( conflictResult == null )
return ;
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
if ( conflictResult != notification )
DeleteFromDb ( typeof ( Notification ). Name , conflictResult . ID );
2025-04-03 13:59:07 +02:00
OverwriteAndUpdateDb ( null , null , [ notification ], false );
2022-12-30 10:59:29 -08:00
2025-04-03 13:59:07 +02:00
if ( type == Serialization . NotificationType . Error )
2022-12-30 10:59:29 -08:00
FIXMEGlobal . DataConnection . ApplicationSettings . UnackedError = true ;
2025-04-03 13:59:07 +02:00
else if ( type == Serialization . NotificationType . Warning )
2022-12-30 10:59:29 -08:00
FIXMEGlobal . DataConnection . ApplicationSettings . UnackedWarning = true ;
}
2024-03-15 16:51:01 +01:00
FIXMEGlobal . NotificationUpdateService . IncrementLastNotificationUpdateId ();
2022-12-30 10:59:29 -08:00
FIXMEGlobal . StatusEventNotifyer . SignalNewEvent ();
}
//Workaround to clean up the database after invalid settings update
public void FixInvalidBackupId ()
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2025-04-03 13:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ( tr ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
cmd . SetCommandAndParameters ( @"DELETE FROM ""Option"" WHERE ""BackupID"" = @BackupId" )
. SetParameterValue ( "@BackupId" , - 1 )
. ExecuteNonQuery ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = @BackupId" )
. SetParameterValue ( "@BackupId" , - 1 )
. ExecuteNonQuery ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""Filter"" WHERE ""BackupID"" = @BackupId" )
. SetParameterValue ( "@BackupId" , - 1 )
. ExecuteNonQuery ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""Source"" WHERE ""BackupID"" = @BackupId" )
. SetParameterValue ( "@BackupId" , - 1 )
. ExecuteNonQuery ();
cmd . SetCommandAndParameters ( @"DELETE FROM ""Schedule"" WHERE ""Tags"" = @Tag" )
. SetParameterValue ( "@Tag" , "ID=-1" )
. ExecuteNonQuery ();
2022-12-30 10:59:29 -08:00
tr . Commit ();
}
ApplicationSettings . FixedInvalidBackupId = true ;
}
public string [] GetUISettingsSchemes ()
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
( rd ) => ConvertToString ( rd , 0 ) ?? "" ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""" ))
2022-12-30 10:59:29 -08:00
. ToArray ();
}
public IDictionary < string , string > GetUISettings ( string scheme )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb (
( rd ) => new KeyValuePair < string , string >(
ConvertToString ( rd , 0 ) ?? "" ,
ConvertToString ( rd , 1 ) ?? ""
),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = @Scheme" )
. SetParameterValue ( "@Scheme" , scheme ))
2022-12-30 10:59:29 -08:00
. GroupBy ( x => x . Key )
. ToDictionary ( x => x . Key , x => x . Last (). Value );
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
public void SetUISettings ( string scheme , IDictionary < string , string? > values , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = @Scheme" )
. SetParameterValue ( "@Scheme" , scheme ),
2022-12-30 10:59:29 -08:00
values ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (@Scheme, @Key, @Value)" ),
( cmd , f ) =>
2024-06-07 15:56:43 +02:00
{
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( "@Scheme" , scheme )
. SetParameterValue ( "@Key" , f . Key ?? "" )
. SetParameterValue ( "@Value" , f . Value ?? "" );
2022-12-30 10:59:29 -08:00
}
2024-06-07 15:56:43 +02:00
);
2022-12-30 10:59:29 -08:00
if ( tr != null )
tr . Commit ();
}
}
2025-03-03 17:39:25 +01:00
public void UpdateUISettings ( string scheme , IDictionary < string , string? > values , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
lock ( m_lock )
2025-04-14 12:04:00 +02:00
using ( var tr = transaction == null ? m_connection . BeginTransactionSafe () : null )
2022-12-30 10:59:29 -08:00
{
OverwriteAndUpdateDb (
tr ,
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = @Scheme AND ""Key"" IN (@Keys)" )
. SetParameterValue ( "@Scheme" , scheme )
. ExpandInClauseParameter ( "@Keys" , values . Keys ),
2022-12-30 10:59:29 -08:00
values . Where ( x => x . Value != null ),
2025-04-03 13:59:07 +02:00
cmd => cmd . SetCommandAndParameters ( @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (@Scheme, @Key, @Value)" ),
( cmd , f ) =>
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( "@Scheme" , scheme )
. SetParameterValue ( "@Key" , f . Key ?? "" )
. SetParameterValue ( "@Value" , f . Value ?? "" );
2022-12-30 10:59:29 -08:00
}
);
if ( tr != null )
tr . Commit ();
}
}
public TempFile [] GetTempFiles ()
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
return ReadFromDb < TempFile >( null ). ToArray ();
}
public void DeleteTempFile ( long id )
{
2024-06-07 15:56:43 +02:00
lock ( m_lock )
2022-12-30 10:59:29 -08:00
DeleteFromDb ( typeof ( TempFile ). Name , id );
}
public long RegisterTempFile ( string origin , string path , DateTime expires )
{
2024-06-07 15:56:43 +02:00
var tempfile = new TempFile ()
{
2022-12-30 10:59:29 -08:00
Timestamp = DateTime . Now ,
Origin = origin ,
Path = path ,
Expires = expires
};
2025-04-03 13:59:07 +02:00
OverwriteAndUpdateDb ( null , null , [ tempfile ], false );
2022-12-30 10:59:29 -08:00
return tempfile . ID ;
}
public void PurgeLogData ( DateTime purgeDate )
{
var t = Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( purgeDate );
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2025-04-03 13:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ( tr ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
cmd . SetCommandAndParameters ( @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < @Time" )
. SetParameterValue ( "@Time" , t )
. ExecuteNonQuery ();
2022-12-30 10:59:29 -08:00
tr . Commit ();
}
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
private static DateTime ConvertToDateTime ( IDataReader rd , int index )
2022-12-30 10:59:29 -08:00
{
var unixTime = ConvertToInt64 ( rd , index );
return unixTime == 0 ? new DateTime ( 0 ) : Library . Utility . Utility . EPOCH . AddSeconds ( unixTime );
}
2025-03-03 17:39:25 +01:00
private static bool ConvertToBoolean ( IDataReader rd , int index )
2022-12-30 10:59:29 -08:00
{
return ConvertToInt64 ( rd , index ) == 1 ;
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
private static string? ConvertToString ( IDataReader rd , int index )
2022-12-30 10:59:29 -08:00
{
var r = rd . GetValue ( index );
return r == null || r == DBNull . Value ? null : r . ToString ();
}
2025-03-03 17:39:25 +01:00
private static long ConvertToInt64 ( IDataReader rd , int index )
2022-12-30 10:59:29 -08:00
{
try
{
if (! rd . IsDBNull ( index ))
return rd . GetInt64 ( index );
}
catch
{
}
return - 1 ;
}
2025-03-03 17:39:25 +01:00
private static long ExecuteScalarInt64 ( IDbCommand cmd , long defaultValue = - 1 )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
using ( var rd = cmd . ExecuteReader ())
2022-12-30 10:59:29 -08:00
return rd . Read () ? ConvertToInt64 ( rd , 0 ) : defaultValue ;
}
2025-03-03 17:39:25 +01:00
private static string? ExecuteScalarString ( IDbCommand cmd )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
using ( var rd = cmd . ExecuteReader ())
2022-12-30 10:59:29 -08:00
return rd . Read () ? ConvertToString ( rd , 0 ) : null ;
}
2025-03-03 17:39:25 +01:00
private object? ConvertToEnum ( Type enumType , IDataReader rd , int index , object? @default )
2022-12-30 10:59:29 -08:00
{
try
{
2025-01-14 14:03:48 +01:00
return Enum . Parse ( enumType , ConvertToString ( rd , index ) ?? string . Empty , true );
2022-12-30 10:59:29 -08:00
}
catch
{
}
return @default ;
}
// Overloaded function for legacy functionality
2025-03-03 17:39:25 +01:00
private bool DeleteFromDb ( string tablename , long id , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
return DeleteFromDb ( tablename , id , "ID" , transaction );
}
// New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID)
2025-03-03 17:39:25 +01:00
private bool DeleteFromDb ( string tablename , long id , string identifier , IDbTransaction ? transaction = null )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
if ( transaction == null )
2022-12-30 10:59:29 -08:00
{
2025-04-14 12:04:00 +02:00
using ( var tr = m_connection . BeginTransactionSafe ())
2022-12-30 10:59:29 -08:00
{
var r = DeleteFromDb ( tablename , id , tr );
tr . Commit ();
return r ;
}
}
else
{
2025-04-03 13:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
cmd . SetCommandAndParameters ( string . Format ( CultureInfo . InvariantCulture , @"DELETE FROM ""{0}"" WHERE ""{1}""=@Value" , tablename , identifier ))
. SetParameterValue ( "@Value" , id );
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
var r = cmd . ExecuteNonQuery ();
// Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem.
if ( identifier == "ID" && r > 1 )
throw new Exception ( string . Format ( "Too many records attempted deleted from table {0} for id {1}: {2}" , tablename , id , r ));
return r == 1 ;
}
}
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
private static IEnumerable < T > Read < T >( IDbCommand cmd , Func < IDataReader , T > f )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
using ( var rd = cmd . ExecuteReader ())
while ( rd . Read ())
2022-12-30 10:59:29 -08:00
yield return f ( rd );
}
2024-06-07 15:56:43 +02:00
2025-03-03 17:39:25 +01:00
private static IEnumerable < T > Read < T >( IDataReader rd , Func < T > f )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
while ( rd . Read ())
2022-12-30 10:59:29 -08:00
yield return f ();
}
private System . Reflection . PropertyInfo [] GetORMFields < T >()
{
2024-06-07 15:56:43 +02:00
var flags =
System . Reflection . BindingFlags . FlattenHierarchy |
2022-12-30 10:59:29 -08:00
System . Reflection . BindingFlags . Instance |
System . Reflection . BindingFlags . Public ;
var supportedPropertyTypes = new Type [] {
typeof ( long ),
typeof ( string ),
typeof ( bool ),
typeof ( DateTime )
};
2024-06-07 15:56:43 +02:00
return
2022-12-30 10:59:29 -08:00
( from n in typeof ( T ). GetProperties ( flags )
2024-06-07 15:56:43 +02:00
where supportedPropertyTypes . Contains ( n . PropertyType ) || n . PropertyType . IsEnum
select n ). ToArray ();
2022-12-30 10:59:29 -08:00
}
2025-04-03 13:59:07 +02:00
private IEnumerable < T > ReadFromDb < T >( Action < IDbCommand >? prep )
2022-12-30 10:59:29 -08:00
{
var properties = GetORMFields < T >();
2025-04-03 13:59:07 +02:00
var sql = string . Format ( CultureInfo . InvariantCulture ,
@"SELECT ""{0}"" FROM ""{1}""" ,
2022-12-30 10:59:29 -08:00
string . Join ( @""", """ , properties . Select ( x => x . Name )),
2025-04-03 13:59:07 +02:00
typeof ( T ). Name
2022-12-30 10:59:29 -08:00
);
2024-06-07 15:56:43 +02:00
return ReadFromDb (( rd ) =>
{
var item = Activator . CreateInstance < T >();
for ( var i = 0 ; i < properties . Length ; i ++)
{
var prop = properties [ i ];
if ( prop . PropertyType . IsEnum )
prop . SetValue ( item , ConvertToEnum ( prop . PropertyType , rd , i , Enum . GetValues ( prop . PropertyType ). GetValue ( 0 )), null );
else if ( prop . PropertyType == typeof ( string ))
prop . SetValue ( item , ConvertToString ( rd , i ), null );
else if ( prop . PropertyType == typeof ( long ))
prop . SetValue ( item , ConvertToInt64 ( rd , i ), null );
else if ( prop . PropertyType == typeof ( bool ))
prop . SetValue ( item , ConvertToBoolean ( rd , i ), null );
else if ( prop . PropertyType == typeof ( DateTime ))
prop . SetValue ( item , ConvertToDateTime ( rd , i ), null );
}
2022-12-30 10:59:29 -08:00
2024-06-07 15:56:43 +02:00
return item ;
2025-04-03 13:59:07 +02:00
},
cmd =>
{
cmd . SetCommandAndParameters ( sql );
if ( prep != null )
prep ( cmd );
});
2022-12-30 10:59:29 -08:00
}
2025-04-03 13:59:07 +02:00
private void OverwriteAndUpdateDb < T >( IDbTransaction ? transaction , Action < IDbCommand >? deletePrep , IEnumerable < T > values , bool updateExisting )
2022-12-30 10:59:29 -08:00
{
var properties = GetORMFields < T >();
2025-01-14 14:03:48 +01:00
var idfield = properties . FirstOrDefault ( x => x . Name == "ID" )
?? throw new Exception ( "No ID field found in type " + typeof ( T ). Name );
2025-04-03 13:59:07 +02:00
var nonIdProps = properties . Where ( x => x . Name != "ID" ). ToArray ();
2022-12-30 10:59:29 -08:00
string sql ;
if ( updateExisting )
{
sql = string . Format (
2025-04-03 13:59:07 +02:00
@"UPDATE ""{0}"" SET {1} WHERE ""ID""= @Id" ,
2022-12-30 10:59:29 -08:00
typeof ( T ). Name ,
2025-04-03 13:59:07 +02:00
string . Join ( @", " , nonIdProps . Select ( x => @ $"""{x.Name}"" = @{x.Name}" ))
2022-12-30 10:59:29 -08:00
);
2025-01-14 14:03:48 +01:00
properties = properties . Append ( idfield ). ToArray ();
2022-12-30 10:59:29 -08:00
}
else
{
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
sql = string . Format (
@"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})" ,
typeof ( T ). Name ,
2025-04-03 13:59:07 +02:00
string . Join ( @""", """ , nonIdProps . Select ( x => x . Name )),
string . Join ( @", " , nonIdProps . Select ( x => $"@{x.Name}" ))
2022-12-30 10:59:29 -08:00
);
}
2025-04-03 13:59:07 +02:00
OverwriteAndUpdateDb ( transaction , deletePrep , values ,
cmd => cmd . SetCommandAndParameters ( sql ),
( cmd , item ) =>
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
foreach ( var p in properties )
2025-01-14 14:03:48 +01:00
{
2025-04-03 17:05:13 +02:00
if (! updateExisting && p == idfield )
continue ;
2025-04-03 13:59:07 +02:00
var val = p . GetValue ( item , null );
if ( val != null )
{
if ( p . PropertyType . IsEnum )
val = val . ToString ();
else if ( p . PropertyType == typeof ( DateTime ))
val = Library . Utility . Utility . NormalizeDateTimeToEpochSeconds (( DateTime ) val );
}
2022-12-30 10:59:29 -08:00
2025-04-03 13:59:07 +02:00
cmd . SetParameterValue ( $"@{p.Name}" , val );
}
});
2022-12-30 10:59:29 -08:00
if (! updateExisting && values . Count () == 1 && idfield != null )
2025-04-03 16:38:51 +02:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 16:38:51 +02:00
cmd . SetCommandAndParameters ( @"SELECT last_insert_rowid();" );
2022-12-30 10:59:29 -08:00
if ( idfield . PropertyType == typeof ( string ))
idfield . SetValue ( values . First (), ExecuteScalarString ( cmd ), null );
else
idfield . SetValue ( values . First (), ExecuteScalarInt64 ( cmd ), null );
}
}
2024-06-07 15:56:43 +02:00
2025-04-03 13:59:07 +02:00
private IEnumerable < T > ReadFromDb < T >( Func < IDataReader , T > f , Action < IDbCommand >? prep )
2022-12-30 10:59:29 -08:00
{
2024-06-07 15:56:43 +02:00
using ( var cmd = m_connection . CreateCommand ())
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
if ( prep != null )
prep ( cmd );
2022-12-30 10:59:29 -08:00
return Read ( cmd , f ). ToArray ();
}
}
2024-06-07 15:56:43 +02:00
2025-04-03 13:59:07 +02:00
private void OverwriteAndUpdateDb < T >( IDbTransaction ? transaction , Action < IDbCommand >? deletePrep , IEnumerable < T > values , Action < IDbCommand > insertPrep , Action < IDbCommand , T > insert )
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ( transaction ))
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
if ( deletePrep != null )
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
deletePrep ( cmd );
2022-12-30 10:59:29 -08:00
cmd . ExecuteNonQuery ();
}
2024-06-07 15:56:43 +02:00
2025-04-03 13:59:07 +02:00
insertPrep ( cmd );
foreach ( var v in values )
2022-12-30 10:59:29 -08:00
{
2025-04-03 13:59:07 +02:00
insert ( cmd , v );
2022-12-30 10:59:29 -08:00
cmd . ExecuteNonQuery ();
}
}
}
2024-06-07 15:56:43 +02:00
2024-08-19 13:51:14 -03:00
/// <summary>
/// Encrypts sensitive fields
/// </summary>
/// <param name="fieldName">The fieldname used to determine if it will be encrypted</param>
/// <param name="fieldValue">The field value</param>
2024-10-24 15:56:39 +02:00
/// <param name="key">The encryption key</param>
2024-08-20 17:08:32 +02:00
/// <returns>The encrypted string or the original value</returns>
2025-01-14 14:03:48 +01:00
private static string? EncryptSensitiveFields ( string fieldName , string fieldValue , EncryptedFieldHelper . KeyInstance ? key )
2024-08-19 13:51:14 -03:00
{
if ( fieldValue != null )
2024-08-20 17:08:32 +02:00
return _encryptedFields . Contains ( fieldName )
2024-10-24 15:56:39 +02:00
? EncryptedFieldHelper . Encrypt ( fieldValue , key )
2024-08-20 17:08:32 +02:00
: fieldValue ;
return null ;
2024-08-19 13:51:14 -03:00
}
/// <summary>
/// Decrypts sensitive fields
/// </summary>
/// <param name="fieldValue">The field value</param>
2024-10-24 15:56:39 +02:00
/// <param name="key">The encryption key</param>
2024-08-20 17:08:32 +02:00
/// <returns>The decrypted string</returns>
2025-01-14 14:03:48 +01:00
private static string? DecryptSensitiveFields ( string? fieldValue , EncryptedFieldHelper . KeyInstance ? key )
2024-08-19 13:51:14 -03:00
{
if ( fieldValue != null )
2024-08-20 17:08:32 +02:00
return EncryptedFieldHelper . IsEncryptedString ( fieldValue )
2024-10-24 15:56:39 +02:00
? EncryptedFieldHelper . Decrypt ( fieldValue , key )
2024-08-20 17:08:32 +02:00
: fieldValue ;
return null ;
2024-08-19 13:51:14 -03:00
}
2022-12-30 10:59:29 -08:00
#region IDisposable implementation
public void Dispose ()
{
2024-08-20 21:10:54 +02:00
try { m_errorcmd ?. Dispose (); }
catch { }
2022-12-30 10:59:29 -08:00
2024-08-20 21:10:54 +02:00
try { m_connection ?. Dispose (); }
catch { }
2022-12-30 10:59:29 -08:00
}
#endregion
}
2024-06-07 15:56:43 +02:00
2022-12-30 10:59:29 -08:00
}