2025-03-13 23:06:32 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-20 15:54:23 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-03-13 23:06:32 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-20 15:54:23 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-03-13 23:06:32 +01:00
// all copies or substantial portions of the Software.
2025-05-20 15:54:23 +02:00
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2025-04-03 15:46:20 +02:00
#nullable enable
2016-12-29 22:41:13 +01:00
using System ;
using System.Collections.Generic ;
2025-05-20 15:54:23 +02:00
using System.Threading.Tasks ;
2019-09-29 20:16:28 -07:00
using Duplicati.Library.Utility ;
2025-05-20 15:59:07 +02:00
using Microsoft.Data.Sqlite ;
2016-12-29 22:41:13 +01:00
namespace Duplicati.Library.Main.Database
{
internal class LocalPurgeDatabase : LocalDeleteDatabase
{
2025-05-20 16:15:37 +02:00
public static new async Task < LocalPurgeDatabase > CreateAsync ( string path , long pagecachesize )
2016-12-29 22:41:13 +01:00
{
2025-05-20 15:54:23 +02:00
var db = new LocalPurgeDatabase ();
db = ( LocalPurgeDatabase ) await CreateLocalDatabaseAsync ( db , path , "ListBrokenFiles" , false , pagecachesize );
db . ShouldCloseConnection = true ;
return db ;
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:15:37 +02:00
public static new async Task < LocalPurgeDatabase > CreateAsync ( LocalDatabase dbparent )
2017-01-05 16:46:01 +01:00
{
2025-05-20 15:54:23 +02:00
var dbnew = new LocalPurgeDatabase ();
dbnew = ( LocalPurgeDatabase ) await CreateLocalDatabaseAsync ( dbparent , dbnew );
return dbnew ;
2017-01-05 16:46:01 +01:00
}
2025-05-20 16:15:37 +02:00
public async Task < string > GetRemoteVolumeNameForFileset ( long id )
2016-12-29 22:41:13 +01:00
{
2025-05-20 15:59:07 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""B"".""Name"" FROM ""Fileset"" A, ""RemoteVolume"" B WHERE ""A"".""VolumeID"" = ""B"".""ID"" AND ""A"".""ID"" = @FilesetId " )
2025-04-02 16:20:12 +02:00
. SetParameterValue ( "@FilesetId" , id );
2025-05-20 16:15:37 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
if (! await rd . ReadAsync ())
2025-03-13 23:06:32 +01:00
throw new Exception ( $"No remote volume found for fileset with id {id}" );
2016-12-29 22:41:13 +01:00
else
2025-04-03 15:46:20 +02:00
return rd . ConvertValueToString ( 0 ) ?? throw new Exception ( $"Remote volume name for fileset with id {id} is null" );
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:15:37 +02:00
internal async Task < long > CountOrphanFiles ()
2016-12-29 22:41:13 +01:00
{
2025-05-20 15:59:07 +02:00
using ( var cmd = m_connection . CreateCommand ())
2025-05-20 16:15:37 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ( @"SELECT COUNT(*) FROM ""FileLookup"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""FileID"" FROM ""FilesetEntry"")" ))
if ( await rd . ReadAsync ())
2016-12-29 22:41:13 +01:00
return rd . ConvertValueToInt64 ( 0 , 0 );
else
return 0 ;
}
public interface ITemporaryFileset : IDisposable
{
long ParentID { get ; }
long RemovedFileCount { get ; }
long RemovedFileSize { get ; }
2025-05-09 11:12:29 +02:00
long UpdatedFileCount { get ; }
2016-12-29 22:41:13 +01:00
2025-05-20 16:15:37 +02:00
Task ApplyFilter ( Library . Utility . IFilter filter );
Task ApplyFilter ( Func < SqliteCommand , long , string , int > filtercommand );
Task < Tuple < long , long >> ConvertToPermanentFileset ( string name , DateTime timestamp , bool isFullBackup );
IAsyncEnumerable < KeyValuePair < string , long >> ListAllDeletedFiles ();
2016-12-29 22:41:13 +01:00
}
private class TemporaryFileset : ITemporaryFileset
{
2025-05-20 15:54:23 +02:00
private string m_tablename = null !;
private LocalDatabase m_db = null !;
2016-12-29 22:41:13 +01:00
public long ParentID { get ; private set ; }
public long RemovedFileCount { get ; private set ; }
public long RemovedFileSize { get ; private set ; }
2025-05-09 11:12:29 +02:00
public long UpdatedFileCount { get ; private set ; }
2016-12-29 22:41:13 +01:00
2025-05-20 15:54:23 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateAsync instead.")]
2025-05-20 15:59:07 +02:00
public TemporaryFileset ( long parentid , LocalPurgeDatabase parentdb , SqliteConnection connection , SqliteTransaction transaction )
2016-12-29 22:41:13 +01:00
{
2025-05-20 15:54:23 +02:00
throw new NotImplementedException ( "Use CreateAsync instead" );
}
2016-12-29 22:41:13 +01:00
2025-05-20 15:54:23 +02:00
private TemporaryFileset () { }
public static async Task < TemporaryFileset > CreateAsync ( long parentid , LocalDatabase db )
{
var tempf = new TemporaryFileset ()
{
ParentID = parentid ,
m_db = db ,
m_tablename = "TempDeletedFilesTable-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ())
};
using ( var cmd = db . Connection . CreateCommand ())
2025-05-20 16:15:37 +02:00
await cmd . ExecuteNonQueryAsync ( $@"CREATE TEMPORARY TABLE ""{tempf.m_tablename}"" (""FileID"" INTEGER PRIMARY KEY) " );
2025-05-20 15:54:23 +02:00
return tempf ;
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:15:37 +02:00
public async Task ApplyFilter ( Func < SqliteCommand , long , string , int > filtercommand )
2016-12-29 22:41:13 +01:00
{
2025-05-09 11:12:29 +02:00
int updated ;
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
2025-05-09 11:12:29 +02:00
updated = filtercommand ( cmd , ParentID , m_tablename );
2025-03-14 12:13:17 +01:00
2025-05-20 16:15:37 +02:00
await PostFilterChecks ( updated );
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:15:37 +02:00
public async Task ApplyFilter ( Library . Utility . IFilter filter )
2016-12-29 22:41:13 +01:00
{
2019-09-29 20:16:28 -07:00
if ( Library . Utility . Utility . IsFSCaseSensitive && filter is FilterExpression expression && expression . Type == Duplicati . Library . Utility . FilterType . Simple )
2016-12-29 22:41:13 +01:00
{
// File list based
2019-09-29 09:51:14 -07:00
// unfortunately we cannot do this if the filesystem is not case-sensitive as
2016-12-29 22:41:13 +01:00
// SQLite only supports ASCII compares
2019-09-29 20:16:28 -07:00
var p = expression . GetSimpleList ();
2016-12-29 22:41:13 +01:00
var filenamestable = "Filenames-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
2016-12-29 22:41:13 +01:00
{
2025-05-20 16:15:37 +02:00
await cmd . ExecuteNonQueryAsync ( $@"CREATE TEMPORARY TABLE ""{filenamestable}"" (""Path"" TEXT NOT NULL) " );
await cmd . SetCommandAndParameters ( $@"INSERT INTO ""{filenamestable}"" (""Path"") VALUES (@Path)" )
. PrepareAsync ();
2016-12-29 22:41:13 +01:00
foreach ( var s in p )
2025-05-20 16:15:37 +02:00
await cmd . SetParameterValue ( "@Path" , s )
. ExecuteNonQueryAsync ();
2016-12-29 22:41:13 +01:00
2025-05-20 16:15:37 +02:00
await cmd . SetCommandAndParameters ( $@"INSERT INTO ""{m_tablename}"" (""FileID"") SELECT DISTINCT ""A"".""FileID"" FROM ""FilesetEntry"" A, ""File"" B WHERE ""A"".""FilesetID"" = @FilesetId AND ""A"".""FileID"" = ""B"".""ID"" AND ""B"".""Path"" IN ""{filenamestable}""" )
2025-04-02 16:20:12 +02:00
. SetParameterValue ( "@FilesetId" , ParentID )
2025-05-20 16:15:37 +02:00
. ExecuteNonQueryAsync ();
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{filenamestable}"" " );
2016-12-29 22:41:13 +01:00
}
}
else
{
// Do row-wise iteration
2025-03-14 14:34:56 +01:00
var values = new object [ 2 ];
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
using ( var cmd2 = m_db . Connection . CreateCommand ())
2016-12-29 22:41:13 +01:00
{
2025-05-20 16:03:12 +02:00
cmd2 . SetCommandAndParameters ( $@"INSERT INTO ""{m_tablename}"" (""FileID"") VALUES (@FileId)" );
2025-04-02 23:20:58 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""B"".""Path"", ""A"".""FileID"" FROM ""FilesetEntry"" A, ""File"" B WHERE ""A"".""FilesetID"" = @FilesetId AND ""A"".""FileID"" = ""B"".""ID"" " )
2025-04-02 16:20:12 +02:00
. SetParameterValue ( "@FilesetId" , ParentID );
2025-05-20 16:15:37 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2016-12-29 22:41:13 +01:00
{
rd . GetValues ( values );
if ( values [ 0 ] != null && values [ 0 ] != DBNull . Value && Library . Utility . FilterExpression . Matches ( filter , values [ 0 ]. ToString ()))
{
2025-05-20 16:15:37 +02:00
await cmd2 . SetParameterValue ( "@FileId" , values [ 1 ])
. ExecuteNonQueryAsync ();
2016-12-29 22:41:13 +01:00
}
}
}
}
2025-05-20 16:15:37 +02:00
await PostFilterChecks ( 0 );
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:15:37 +02:00
private async Task PostFilterChecks ( int updated )
2016-12-29 22:41:13 +01:00
{
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
2016-12-29 22:41:13 +01:00
{
2025-05-09 11:12:29 +02:00
UpdatedFileCount = updated ;
2025-05-20 16:15:37 +02:00
RemovedFileCount = await cmd . ExecuteScalarInt64Async ( $@"SELECT COUNT(*) FROM ""{m_tablename}""" , 0 );
RemovedFileSize = await cmd . ExecuteScalarInt64Async ( $@"
2025-05-20 15:54:23 +02:00
SELECT
SUM(""C"".""Length"")
2025-05-09 11:12:29 +02:00
FROM
""{m_tablename}"" A, ""FileLookup"" B, ""Blockset"" C, ""Metadataset"" D
WHERE
""A"".""FileID"" = ""B"".""ID""
2025-05-20 15:54:23 +02:00
AND(""B"".""BlocksetID"" = ""C"".""ID"" OR(""B"".""MetadataID"" = ""D"".""ID"" AND ""D"".""BlocksetID"" = ""C"".""ID""))
2025-05-20 16:15:37 +02:00
" , 0 );
var filesetcount = await cmd . ExecuteScalarInt64Async ( $@"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FilesetID"" = {ParentID}" , 0 );
2016-12-29 22:41:13 +01:00
if ( filesetcount == RemovedFileCount )
2025-03-14 14:34:56 +01:00
throw new Interface . UserInformationException ( $"Refusing to purge {RemovedFileCount} files from fileset with ID {ParentID}, as that would remove the entire fileset.\nTo delete a fileset, use the \" delete \ " command." , "PurgeWouldRemoveEntireFileset" );
2016-12-29 22:41:13 +01:00
}
}
2025-05-20 16:15:37 +02:00
public async Task < Tuple < long , long >> ConvertToPermanentFileset ( string name , DateTime timestamp , bool isFullBackup )
2019-09-01 13:13:45 -04:00
{
2025-05-20 16:15:37 +02:00
var remotevolid = await m_db . RegisterRemoteVolume ( name , RemoteVolumeType . Files , RemoteVolumeState . Temporary );
var filesetid = await m_db . CreateFileset ( remotevolid , timestamp );
await m_db . UpdateFullBackupStateInFileset ( filesetid , isFullBackup );
2019-09-10 12:55:29 -04:00
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
await cmd . SetCommandAndParameters ( $@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") SELECT @TargetFilesetId, ""FileID"", ""LastModified"" FROM ""FilesetEntry"" WHERE ""FilesetID"" = @SourceFilesetId AND ""FileID"" NOT IN ""{m_tablename}"" " )
2025-04-02 16:20:12 +02:00
. SetParameterValue ( "@TargetFilesetId" , filesetid )
. SetParameterValue ( "@SourceFilesetId" , ParentID )
2025-05-20 16:15:37 +02:00
. ExecuteNonQueryAsync ();
2016-12-29 22:41:13 +01:00
return new Tuple < long , long >( remotevolid , filesetid );
}
2025-03-14 12:13:17 +01:00
2025-05-20 16:15:37 +02:00
public async IAsyncEnumerable < KeyValuePair < string , long >> ListAllDeletedFiles ()
2016-12-29 22:41:13 +01:00
{
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
using ( var rd = await cmd . ExecuteReaderAsync ( $@"SELECT ""B"".""Path"", ""C"".""Length"" FROM ""{m_tablename}"" A, ""File"" B, ""Blockset"" C WHERE ""A"".""FileID"" = ""B"".""ID"" AND ""B"".""BlocksetID"" = ""C"".""ID"" " ))
while ( await rd . ReadAsync ())
2025-04-03 15:46:20 +02:00
yield return new KeyValuePair < string , long >( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToInt64 ( 1 ));
2016-12-29 22:41:13 +01:00
}
public void Dispose ()
2025-05-20 16:15:37 +02:00
{
DisposeAsync (). Await ();
}
public async Task DisposeAsync ()
2016-12-29 22:41:13 +01:00
{
try
{
2025-05-20 16:15:37 +02:00
using ( var cmd = m_db . Connection . CreateCommand ())
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tablename}""" );
2016-12-29 22:41:13 +01:00
}
catch
{
}
}
}
2025-03-14 12:13:17 +01:00
}
2016-12-29 22:41:13 +01:00
}