2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-01-28 08:54:50 +01:00
// https://duplicati.com, hello@duplicati.com
2025-05-22 05:59:01 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-01-28 08:54:50 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-22 05:59:01 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-28 08:54:50 +01:00
// all copies or substantial portions of the Software.
2025-05-22 05:59:01 +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.
2016-12-29 22:41:13 +01:00
using System ;
using System.Linq ;
2025-03-14 10:41:14 +01:00
using System.Threading.Tasks ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
2025-05-09 11:12:29 +02:00
using Duplicati.Library.Main.Volumes ;
2025-01-28 08:54:50 +01:00
using Duplicati.Library.Utility ;
2025-05-22 05:59:18 +02:00
using Microsoft.Data.Sqlite ;
2016-12-29 22:41:13 +01:00
namespace Duplicati.Library.Main.Operation
{
internal class PurgeFilesHandler
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < PurgeFilesHandler >();
2019-10-19 10:56:21 -07:00
protected readonly Options m_options ;
protected readonly PurgeFilesResults m_result ;
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
public PurgeFilesHandler ( Options options , PurgeFilesResults result )
2016-12-29 22:41:13 +01:00
{
m_options = options ;
m_result = result ;
}
2025-03-14 10:41:14 +01:00
public async Task RunAsync ( IBackendManager backendManager , IFilter filter )
2016-12-29 22:41:13 +01:00
{
2017-01-05 16:46:01 +01:00
if ( filter == null || filter . Empty )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Cannot purge with an empty filter, as that would cause all files to be removed.\nTo remove an entire backup set, use the \"delete\" command." , "EmptyFilterPurgeNotAllowed" );
2017-01-05 16:46:01 +01:00
if (! System . IO . File . Exists ( m_options . Dbpath ))
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( string . Format ( "Database file does not exist: {0}" , m_options . Dbpath ), "DatabaseDoesNotExist" );
2018-01-21 02:11:27 -06:00
2025-06-19 11:41:39 +02:00
await using var db = await Database . LocalPurgeDatabase . CreateAsync ( m_options . Dbpath , null , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-06-18 11:18:25 +02:00
await DoRunAsync ( backendManager , db , filter , null , 0 , 1 ). ConfigureAwait ( false );
await db
2025-06-19 09:47:03 +02:00
. VerifyConsistency ( m_options . Blocksize , m_options . BlockhashSize , true , m_result . TaskControl . ProgressToken )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
2025-05-22 05:59:18 +02:00
public Task RunAsync ( IBackendManager backendManager , Database . LocalPurgeDatabase db , float pgoffset , float pgspan , Func < SqliteCommand , long , string , Task < int >> filtercommand )
2025-03-14 10:41:14 +01:00
=> DoRunAsync ( backendManager , db , null , filtercommand , pgoffset , pgspan );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
private async Task DoRunAsync ( IBackendManager backendManager , Database . LocalPurgeDatabase db , IFilter filter , Func < SqliteCommand , long , string , Task < int >> filtercommand , float pgoffset , float pgspan )
2016-12-29 22:41:13 +01:00
{
m_result . OperationProgressUpdater . UpdatePhase ( OperationPhase . PurgeFiles_Begin );
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "StartingPurge" , "Starting purge operation" );
2016-12-29 22:41:13 +01:00
var doCompactStep = ! m_options . NoAutoCompact && filtercommand == null ;
2025-06-19 09:47:03 +02:00
if ( await db . PartiallyRecreated ( m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2025-01-28 08:54:50 +01:00
throw new UserInformationException ( "The purge command does not work on partially recreated databases" , "PurgeNotAllowedOnPartialDatabase" );
2017-01-05 16:46:01 +01:00
2025-06-19 09:47:03 +02:00
if ( await db . RepairInProgress ( m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ) && filtercommand == null )
2025-01-28 08:54:50 +01:00
throw new UserInformationException ( string . Format ( "The purge command does not work on an incomplete database, try the {0} operation." , "purge-broken-files" ), "PurgeNotAllowedOnIncompleteDatabase" );
2017-01-05 16:46:01 +01:00
2025-05-22 05:59:18 +02:00
var versions = await db
2025-06-19 09:47:03 +02:00
. GetFilesetIDs ( m_options . Time , m_options . Version , false , m_result . TaskControl . ProgressToken )
2025-05-22 05:59:18 +02:00
. OrderByDescending ( x => x )
2025-06-19 09:47:03 +02:00
. ToArrayAsync ( cancellationToken : m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
if ( versions . Length <= 0 )
throw new UserInformationException ( "No filesets matched the supplied time or versions" , "NoFilesetFoundForTimeOrVersion" );
2016-12-29 22:41:13 +01:00
2025-06-19 09:47:03 +02:00
var orphans = await db . CountOrphanFiles ( m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
if ( orphans != 0 )
throw new UserInformationException ( string . Format ( "Unable to start the purge process as there are {0} orphan file(s)" , orphans ), "CannotPurgeWithOrphans" );
2016-12-29 22:41:13 +01:00
2025-06-19 09:47:03 +02:00
await Utility . UpdateOptionsFromDb ( db , m_options , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-19 09:47:03 +02:00
await Utility . VerifyOptionsAndUpdateDatabase ( db , m_options , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
if ( filtercommand == null )
{
2025-06-19 09:47:03 +02:00
await db . VerifyConsistency ( m_options . Blocksize , m_options . BlockhashSize , false , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
if ( m_options . NoBackendverification )
2026-01-27 15:01:34 +01:00
await FilelistProcessor . VerifyLocalList ( backendManager , db , m_options . Dryrun , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
else
2025-06-19 09:47:03 +02:00
await FilelistProcessor . VerifyRemoteList ( backendManager , m_options , db , m_result . BackendWriter , null , null , logErrors : true , verifyMode : FilelistProcessor . VerifyMode . VerifyStrict , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
}
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
var filesets = await db
2025-06-19 09:47:03 +02:00
. FilesetTimes ( m_result . TaskControl . ProgressToken )
2025-05-22 05:59:18 +02:00
. OrderByDescending ( x => x . Value )
2025-06-19 09:47:03 +02:00
. ToArrayAsync ( cancellationToken : m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
var versionprogress = (( doCompactStep ? 0.75f : 1.0f ) / versions . Length ) * pgspan ;
var currentprogress = pgoffset ;
var progress = 0 ;
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
m_result . OperationProgressUpdater . UpdatePhase ( OperationPhase . PurgeFiles_Process );
m_result . OperationProgressUpdater . UpdateProgress ( currentprogress );
2025-03-06 12:02:11 +01:00
// If we crash now, it is possible that the remote storage contains partial files
if (! m_options . Dryrun )
2025-06-19 09:47:03 +02:00
await db . TerminatedWithActiveUploads ( m_result . TaskControl . ProgressToken , true ). ConfigureAwait ( false );
2025-03-06 12:02:11 +01:00
2025-01-28 08:54:50 +01:00
// Reverse makes sure we re-write the old versions first
foreach ( var versionid in versions . Reverse ())
{
progress ++;
Logging . Log . WriteVerboseMessage ( LOGTAG , "ProcessingFilelistVolumes" , "Processing filelist volume {0} of {1}" , progress , versions . Length );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
( var _ , var tsOriginal , var ix ) = filesets . Select (( x , i ) => ( x . Key , x . Value , i )). FirstOrDefault ( x => x . Key == versionid );
if ( ix < 0 || tsOriginal . Ticks == 0 )
throw new InvalidProgramException ( string . Format ( "Fileset was reported with id {0}, but could not be found?" , versionid ));
2025-01-28 08:54:50 +01:00
2025-06-19 11:04:09 +02:00
var ts = await FilesetVolumeWriter . ProbeUnusedFilenameName ( db , m_options , tsOriginal , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-19 09:47:03 +02:00
var prevfilename = await db . GetRemoteVolumeNameForFileset ( filesets [ ix ]. Key , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-09-01 13:13:45 -04:00
2025-05-22 05:59:18 +02:00
if ( ix != 0 && filesets [ ix - 1 ]. Value <= ts )
throw new Exception ( string . Format ( "Unable to create a new fileset for {0} because the resulting timestamp {1} is larger than the next timestamp {2}" , prevfilename , ts , filesets [ ix - 1 ]. Value ));
2016-12-29 22:41:13 +01:00
2025-06-19 09:47:03 +02:00
await using ( var tempset = await db . CreateTemporaryFileset ( versionid , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2025-05-22 05:59:18 +02:00
{
2026-01-27 21:15:50 +01:00
tempset . ReducedPurgeStatistics = m_options . ReducedPurgeStatistics ;
2025-05-22 05:59:18 +02:00
if ( filtercommand == null )
2025-06-19 09:47:03 +02:00
await tempset . ApplyFilter ( filter , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
else
2025-06-19 09:47:03 +02:00
await tempset . ApplyFilter ( filtercommand , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
if ( tempset . RemovedFileCount + tempset . UpdatedFileCount == 0 )
{
Logging . Log . WriteInformationMessage ( LOGTAG , "NotWritingNewFileset" , "Not writing a new fileset for {0} as it was not changed" , prevfilename );
currentprogress += versionprogress ;
2025-06-12 08:34:29 +02:00
await db . Transaction
. RollBackAsync ()
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
continue ;
}
else
{
using ( var tf = new TempFile ())
using ( var vol = new FilesetVolumeWriter ( m_options , ts ))
2025-01-28 08:54:50 +01:00
{
2025-06-12 08:34:29 +02:00
var isOriginalFilesetFullBackup = await db
2025-06-19 09:47:03 +02:00
. IsFilesetFullBackup ( tsOriginal , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
var newids = await tempset
2025-06-19 09:47:03 +02:00
. ConvertToPermanentFileset ( vol . RemoteFilename , ts , isOriginalFilesetFullBackup , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
vol . VolumeID = newids . Item1 ;
vol . CreateFilesetFile ( isOriginalFilesetFullBackup );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "ReplacingFileset" , "Replacing fileset {0} with {1} which has with {2} fewer file(s) ({3} reduction)" , prevfilename , vol . RemoteFilename , tempset . RemovedFileCount , Library . Utility . Utility . FormatSizeString ( tempset . RemovedFileSize ));
2016-12-29 22:41:13 +01:00
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. WriteFileset ( vol , newids . Item2 , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
m_result . RemovedFileSize += tempset . RemovedFileSize ;
m_result . RemovedFileCount += tempset . RemovedFileCount ;
m_result . UpdatedFileCount += tempset . UpdatedFileCount ;
m_result . RewrittenFileLists ++;
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
currentprogress += ( versionprogress / 2 );
m_result . OperationProgressUpdater . UpdateProgress ( currentprogress );
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
if ( m_options . Dryrun || m_options . FullResult )
{
2025-06-19 09:47:03 +02:00
await foreach ( var fe in tempset . ListAllDeletedFiles ( m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2025-01-28 08:54:50 +01:00
{
2025-05-22 05:59:18 +02:00
var msg = string . Format ( " Purging file {0} ({1})" , fe . Key , Library . Utility . Utility . FormatSizeString ( fe . Value ));
2016-12-29 22:41:13 +01:00
2025-05-22 05:59:18 +02:00
Logging . Log . WriteProfilingMessage ( LOGTAG , "PurgeFile" , msg );
Logging . Log . WriteVerboseMessage ( LOGTAG , "PurgeFile" , msg );
2016-12-29 22:41:13 +01:00
if ( m_options . Dryrun )
2025-05-22 05:59:18 +02:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldPurgeFile" , msg );
2025-01-28 08:54:50 +01:00
}
if ( m_options . Dryrun )
2025-05-22 05:59:18 +02:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldWriteRemoteFiles" , "Would write files to remote storage" );
Logging . Log . WriteVerboseMessage ( LOGTAG , "WritingRemoteFiles" , "Writing files to remote storage" );
}
if ( m_options . Dryrun )
{
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldUploadAndDelete" , "Would upload file {0} ({1}) and delete file {2}, removing {3} files" , vol . RemoteFilename , Library . Utility . Utility . FormatSizeString ( vol . Filesize ), prevfilename , tempset . RemovedFileCount );
2025-06-12 08:34:29 +02:00
await db . Transaction
. RollBackAsync ()
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
}
else
{
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. UpdateRemoteVolume ( vol . RemoteFilename , RemoteVolumeState . Uploading , - 1 , null , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
var lst = await db
2025-06-19 09:47:03 +02:00
. DropFilesetsFromTable ( new [] { tsOriginal }, m_result . TaskControl . ProgressToken )
. ToArrayAsync ( cancellationToken : m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
foreach ( var f in lst )
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. UpdateRemoteVolume ( f . Key , RemoteVolumeState . Deleting , f . Value , null , m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
2025-06-12 08:34:29 +02:00
await db . Transaction
2025-06-19 09:47:03 +02:00
. CommitAsync ( m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
await backendManager . PutAsync ( vol , null , null , true , async () =>
2025-01-28 08:54:50 +01:00
{
2025-05-22 05:59:18 +02:00
await backendManager . FlushPendingMessagesAsync ( db , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-06-11 09:11:35 +02:00
// This commit mimics the pre-
// Microsoft.Data.Sqlite backend way of not
// passing down a transaction, but maybe is
// not needed anymore.
2025-06-12 08:34:29 +02:00
await db . Transaction
2025-06-19 09:47:03 +02:00
. CommitAsync ( m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 05:59:18 +02:00
}, m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
await backendManager . DeleteAsync ( prevfilename , - 1 , true , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
await backendManager . WaitForEmptyAsync ( db , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
}
}
}
2025-01-28 08:54:50 +01:00
currentprogress += ( versionprogress / 2 );
m_result . OperationProgressUpdater . UpdateProgress ( currentprogress );
}
2025-03-06 12:02:11 +01:00
if (! m_options . Dryrun )
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. TerminatedWithActiveUploads ( m_result . TaskControl . ProgressToken , false )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-01-28 08:54:50 +01:00
if ( doCompactStep )
{
if ( m_result . RewrittenFileLists == 0 )
2016-12-29 22:41:13 +01:00
{
2025-01-28 08:54:50 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "SkippingCompacting" , "Skipping compacting as no new volumes were written" );
}
else
{
m_result . OperationProgressUpdater . UpdateProgress ( pgoffset + ( 0.75f * pgspan ));
m_result . OperationProgressUpdater . UpdatePhase ( OperationPhase . PurgeFiles_Compact );
m_result . CompactResults = new CompactResults ( m_result );
2025-06-19 09:47:03 +02:00
await using var cdb = await Database . LocalDeleteDatabase . CreateAsync ( db , null , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-06-18 11:18:25 +02:00
await new CompactHandler ( m_options , ( CompactResults ) m_result . CompactResults )
. DoCompactAsync ( cdb , true , backendManager )
. ConfigureAwait ( false );
2025-03-14 10:41:14 +01:00
2025-06-19 09:47:03 +02:00
await cdb . Transaction . CommitAsync ( "PostCompact" , true , m_result . TaskControl . ProgressToken );
2016-12-29 22:41:13 +01:00
}
2025-01-28 08:54:50 +01:00
m_result . OperationProgressUpdater . UpdateProgress ( pgoffset + pgspan );
m_result . OperationProgressUpdater . UpdatePhase ( OperationPhase . PurgeFiles_Complete );
2025-05-22 05:59:18 +02:00
await backendManager . WaitForEmptyAsync ( db , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
}
}
}