2025-01-28 08:54:50 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2013-03-27 16:06:45 +01:00
2013-05-08 20:17:07 +02:00
using Duplicati.Library.Main.Database ;
using Duplicati.Library.Main.Volumes ;
2024-12-18 08:27:59 +01:00
using Duplicati.Library.Utility ;
2020-01-19 21:57:08 -06:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2025-01-28 08:54:50 +01:00
using System.Threading ;
using System.Threading.Tasks ;
2013-03-27 16:06:45 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Operation
2013-03-27 16:06:45 +01:00
{
2016-09-15 11:39:27 +02:00
internal class CompactHandler
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < CompactHandler >();
2019-10-19 10:56:21 -07:00
protected readonly Options m_options ;
protected readonly CompactResults m_result ;
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
public CompactHandler ( Options options , CompactResults result )
2016-09-15 11:39:27 +02:00
{
2013-03-27 16:06:45 +01:00
m_options = options ;
2013-05-25 16:40:15 +02:00
m_result = result ;
2016-09-15 11:39:27 +02:00
}
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
public virtual async Task Run ( IBackendManager backendManager )
2013-06-09 16:26:08 +02:00
{
if (! System . IO . File . Exists ( m_options . Dbpath ))
throw new Exception ( string . Format ( "Database file does not exist: {0}" , m_options . Dbpath ));
2024-05-21 15:48:29 +02:00
using ( var db = new LocalDeleteDatabase ( m_options . Dbpath , "Compact" ))
2013-06-09 16:26:08 +02:00
{
2015-11-17 12:37:33 +01:00
var tr = db . BeginTransaction ();
try
2013-06-09 16:26:08 +02:00
{
2015-11-17 12:37:33 +01:00
m_result . SetDatabase ( db );
Utility . UpdateOptionsFromDb ( db , m_options );
2024-05-21 15:48:29 +02:00
Utility . VerifyOptionsAndUpdateDatabase ( db , m_options );
2025-01-28 08:54:50 +01:00
// TODO: Handle transaction scopes better
var ( changed , tr2 ) = DoCompact ( db , false , tr , backendManager ). Await ();
tr = tr2 ;
2024-05-21 15:48:29 +02:00
2015-11-17 12:37:33 +01:00
if ( changed && m_options . UploadVerificationFile )
2025-01-28 08:54:50 +01:00
await FilelistProcessor . UploadVerificationFile ( backendManager , m_options , db , null );
2024-05-21 15:48:29 +02:00
2015-11-17 12:37:33 +01:00
if (! m_options . Dryrun )
2014-07-16 00:57:57 +02:00
{
2024-05-21 15:48:29 +02:00
using ( new Logging . Timer ( LOGTAG , "CommitCompact" , "CommitCompact" ))
2015-11-17 12:37:33 +01:00
tr . Commit ();
if ( changed )
{
2017-08-04 12:09:01 +01:00
db . WriteResults ();
if ( m_options . AutoVacuum )
{
2019-07-29 17:56:43 -07:00
m_result . VacuumResults = new VacuumResults ( m_result );
new VacuumHandler ( m_options , ( VacuumResults ) m_result . VacuumResults ). Run ();
2017-08-04 12:09:01 +01:00
}
2015-11-17 12:37:33 +01:00
}
2014-07-16 00:57:57 +02:00
}
2016-09-15 11:39:27 +02:00
else
tr . Rollback ();
2024-05-21 15:48:29 +02:00
2015-11-17 12:37:33 +01:00
tr = null ;
}
finally
{
if ( tr != null )
try { tr . Rollback (); }
catch { }
2013-06-09 16:26:08 +02:00
}
2013-08-23 22:15:07 +02:00
}
2016-09-15 11:39:27 +02:00
}
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
internal async Task <( bool , System . Data . IDbTransaction )> DoCompact ( LocalDeleteDatabase db , bool hasVerifiedBackend , System . Data . IDbTransaction transaction , IBackendManager backendManager )
2014-05-15 12:47:16 +02:00
{
2025-01-28 08:54:50 +01:00
var cancellationToken = CancellationToken . None ;
2014-05-15 12:47:16 +02:00
var report = db . GetCompactReport ( m_options . VolumeSize , m_options . Threshold , m_options . SmallFileSize , m_options . SmallFileMaxCount , transaction );
2018-03-12 14:07:11 +01:00
report . ReportCompactData ();
2024-05-21 15:48:29 +02:00
2014-05-15 12:47:16 +02:00
if ( report . ShouldReclaim || report . ShouldCompact )
2019-02-16 20:43:05 -08:00
{
2024-05-21 15:48:29 +02:00
// Workaround where we allow a running backendmanager to be used
2025-01-28 08:54:50 +01:00
if (! hasVerifiedBackend )
await FilelistProcessor . VerifyRemoteList ( backendManager , m_options , db , m_result . BackendWriter , true , transaction ). ConfigureAwait ( false );
var newvol = new BlockVolumeWriter ( m_options );
newvol . VolumeID = db . RegisterRemoteVolume ( newvol . RemoteFilename , RemoteVolumeType . Blocks , RemoteVolumeState . Temporary , transaction );
IndexVolumeWriter newvolindex = null ;
if ( m_options . IndexfilePolicy != Options . IndexFileStrategy . None )
2014-05-15 12:47:16 +02:00
{
2025-01-28 08:54:50 +01:00
newvolindex = new IndexVolumeWriter ( m_options );
newvolindex . VolumeID = db . RegisterRemoteVolume ( newvolindex . RemoteFilename , RemoteVolumeType . Index , RemoteVolumeState . Temporary , transaction );
db . AddIndexBlockLink ( newvolindex . VolumeID , newvol . VolumeID , transaction );
}
2020-01-19 21:57:08 -06:00
2025-01-28 08:54:50 +01:00
var blocksInVolume = 0L ;
var buffer = new byte [ m_options . Blocksize ];
var remoteList = db . GetRemoteVolumes (). Where ( n => n . State == RemoteVolumeState . Uploaded || n . State == RemoteVolumeState . Verified ). ToArray ();
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
//These are for bookkeeping
var uploadedVolumes = new List < KeyValuePair < string , long >>();
var deletedVolumes = new List < KeyValuePair < string , long >>();
var downloadedVolumes = new List < KeyValuePair < string , long >>();
2019-02-16 14:15:19 -08:00
2025-01-28 08:54:50 +01:00
//We start by deleting unused volumes to save space before uploading new stuff
List < IRemoteVolume > fullyDeleteable = [];
if ( report . DeleteableVolumes . Any ())
{
var deleteableVolumesAsHashSet = new HashSet < string >( report . DeleteableVolumes );
fullyDeleteable =
remoteList
. Where ( n => deleteableVolumesAsHashSet . Contains ( n . Name ))
. Cast < IRemoteVolume >()
. ToList ();
}
deletedVolumes . AddRange ( DoDelete ( db , backendManager , fullyDeleteable , ref transaction , cancellationToken ));
2019-02-16 14:15:19 -08:00
2025-01-28 08:54:50 +01:00
// This list is used to pick up unused volumes,
// so they can be deleted once the upload of the
// required fragments is complete
var deleteableVolumes = new List < IRemoteVolume >();
2019-02-16 14:15:19 -08:00
2025-01-28 08:54:50 +01:00
if ( report . ShouldCompact )
{
newvolindex ?. StartVolume ( newvol . RemoteFilename );
List < IRemoteVolume > volumesToDownload = [];
if ( report . CompactableVolumes . Any ())
2024-10-21 14:27:18 +02:00
{
2025-01-28 08:54:50 +01:00
var compactableVolumesAsHashSet = new HashSet < string >( report . CompactableVolumes );
volumesToDownload =
2024-10-21 14:27:18 +02:00
remoteList
2025-01-28 08:54:50 +01:00
. Where ( n => compactableVolumesAsHashSet . Contains ( n . Name ))
2024-10-21 14:27:18 +02:00
. Cast < IRemoteVolume >()
. ToList ();
}
2013-04-08 22:24:54 +02:00
2025-01-28 08:54:50 +01:00
using ( var q = db . CreateBlockQueryHelper ( transaction ))
2014-05-15 12:47:16 +02:00
{
2025-02-18 09:18:36 +01:00
await foreach ( var ( tmpfile , hash , size , name ) in backendManager . GetFilesOverlappedAsync ( volumesToDownload , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2014-05-15 12:47:16 +02:00
{
2025-01-28 08:54:50 +01:00
using ( tmpfile )
2014-05-15 12:47:16 +02:00
{
2025-01-28 08:54:50 +01:00
var entry = new RemoteVolume ( name , hash , size );
if (! m_result . TaskControl . ProgressRendevouz (). Await ())
2014-05-15 12:47:16 +02:00
{
2025-01-28 08:54:50 +01:00
backendManager . WaitForEmptyAsync ( db , transaction , cancellationToken ). Await ();
return ( false , transaction );
}
2018-05-12 17:24:33 -07:00
2025-01-28 08:54:50 +01:00
downloadedVolumes . Add ( new KeyValuePair < string , long >( entry . Name , entry . Size ));
var inst = VolumeBase . ParseFilename ( entry . Name );
using ( var f = new BlockVolumeReader ( inst . CompressionModule , tmpfile , m_options ))
{
foreach ( var e in f . Blocks )
2016-09-15 11:39:27 +02:00
{
2025-01-28 08:54:50 +01:00
if ( q . UseBlock ( e . Key , e . Value , transaction ))
2016-09-15 11:39:27 +02:00
{
2025-01-28 08:54:50 +01:00
//TODO: How do we get the compression hint? Reverse query for filename in db?
var s = f . ReadBlock ( e . Key , buffer );
if ( s != e . Value )
throw new Exception ( string . Format ( "Size mismatch problem for block {0}, {1} vs {2}" , e . Key , s , e . Value ));
newvol . AddBlock ( e . Key , buffer , 0 , s , Duplicati . Library . Interface . CompressionHint . Compressible );
if ( newvolindex != null )
newvolindex . AddBlock ( e . Key , e . Value );
db . RegisterDuplicatedBlock ( e . Key , e . Value , newvol . VolumeID , transaction );
blocksInVolume ++;
if ( newvol . Filesize > m_options . VolumeSize )
2016-09-15 11:39:27 +02:00
{
2025-01-28 08:54:50 +01:00
FinishVolumeAndUpload ( db , backendManager , newvol , newvolindex , uploadedVolumes );
2018-05-12 17:24:33 -07:00
2025-01-28 08:54:50 +01:00
newvol = new BlockVolumeWriter ( m_options );
newvol . VolumeID = db . RegisterRemoteVolume ( newvol . RemoteFilename , RemoteVolumeType . Blocks , RemoteVolumeState . Temporary , transaction );
2018-05-12 17:24:33 -07:00
2025-01-28 08:54:50 +01:00
if ( m_options . IndexfilePolicy != Options . IndexFileStrategy . None )
{
newvolindex = new IndexVolumeWriter ( m_options );
newvolindex . VolumeID = db . RegisterRemoteVolume ( newvolindex . RemoteFilename , RemoteVolumeType . Index , RemoteVolumeState . Temporary , transaction );
db . AddIndexBlockLink ( newvolindex . VolumeID , newvol . VolumeID , transaction );
newvolindex . StartVolume ( newvol . RemoteFilename );
}
blocksInVolume = 0 ;
// Wait for the backend to catch up
backendManager . WaitForEmptyAsync ( db , transaction , cancellationToken ). Await ();
2018-05-12 17:24:33 -07:00
2025-01-28 08:54:50 +01:00
// Commit as we have uploaded a volume
if (! m_options . Dryrun )
2016-09-15 11:39:27 +02:00
{
2025-01-28 08:54:50 +01:00
transaction . Commit ();
transaction = db . BeginTransaction ();
2016-09-15 11:39:27 +02:00
}
}
}
}
}
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
deleteableVolumes . Add ( entry );
2016-09-15 11:39:27 +02:00
}
2025-01-28 08:54:50 +01:00
}
if ( blocksInVolume > 0 )
{
FinishVolumeAndUpload ( db , backendManager , newvol , newvolindex , uploadedVolumes );
}
else
{
db . RemoveRemoteVolume ( newvol . RemoteFilename , transaction );
if ( newvolindex != null )
2016-09-15 11:39:27 +02:00
{
2025-01-28 08:54:50 +01:00
db . RemoveRemoteVolume ( newvolindex . RemoteFilename , transaction );
newvolindex . FinishVolume ( null , 0 );
2016-09-15 11:39:27 +02:00
}
}
}
2025-01-28 08:54:50 +01:00
}
else
{
newvolindex ?. Dispose ();
newvol . Dispose ();
}
2019-02-16 14:15:19 -08:00
2025-01-28 08:54:50 +01:00
deletedVolumes . AddRange ( DoDelete ( db , backendManager , deleteableVolumes , ref transaction , cancellationToken ));
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
var downloadSize = downloadedVolumes . Where ( x => x . Value >= 0 ). Aggregate ( 0L , ( a , x ) => a + x . Value );
var deletedSize = deletedVolumes . Where ( x => x . Value >= 0 ). Aggregate ( 0L , ( a , x ) => a + x . Value );
var uploadSize = uploadedVolumes . Where ( x => x . Value >= 0 ). Aggregate ( 0L , ( a , x ) => a + x . Value );
2019-02-16 14:15:19 -08:00
2025-01-28 08:54:50 +01:00
m_result . DeletedFileCount = deletedVolumes . Count ;
m_result . DownloadedFileCount = downloadedVolumes . Count ;
m_result . UploadedFileCount = uploadedVolumes . Count ;
m_result . DeletedFileSize = deletedSize ;
m_result . DownloadedFileSize = downloadSize ;
m_result . UploadedFileSize = uploadSize ;
m_result . Dryrun = m_options . Dryrun ;
2024-05-21 15:48:29 +02:00
2025-01-28 08:54:50 +01:00
if ( m_result . Dryrun )
{
if ( downloadedVolumes . Count == 0 )
Logging . Log . WriteDryrunMessage ( LOGTAG , "CompactResults" , "Would delete {0} files, which would reduce storage by {1}" , m_result . DeletedFileCount , Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize ));
2016-09-15 11:39:27 +02:00
else
2025-01-28 08:54:50 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "CompactResults" , "Would download {0} file(s) with a total size of {1}, delete {2} file(s) with a total size of {3}, and compact to {4} file(s) with a size of {5}, which would reduce storage by {6} file(s) and {7}" ,
m_result . DownloadedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . DownloadedFileSize ),
m_result . DeletedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize ), m_result . UploadedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . UploadedFileSize ),
m_result . DeletedFileCount - m_result . UploadedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize - m_result . UploadedFileSize ));
2016-09-15 11:39:27 +02:00
}
2025-01-28 08:54:50 +01:00
else
{
if ( m_result . DownloadedFileCount == 0 )
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactResults" , "Deleted {0} files, which reduced storage by {1}" , m_result . DeletedFileCount , Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize ));
else
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactResults" , "Downloaded {0} file(s) with a total size of {1}, deleted {2} file(s) with a total size of {3}, and compacted to {4} file(s) with a size of {5}, which reduced storage by {6} file(s) and {7}" ,
m_result . DownloadedFileCount ,
Library . Utility . Utility . FormatSizeString ( downloadSize ),
m_result . DeletedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize ),
m_result . UploadedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . UploadedFileSize ),
m_result . DeletedFileCount - m_result . UploadedFileCount ,
Library . Utility . Utility . FormatSizeString ( m_result . DeletedFileSize - m_result . UploadedFileSize ));
}
backendManager . WaitForEmptyAsync ( db , transaction , cancellationToken ). Await ();
2016-12-01 23:59:54 +01:00
m_result . EndTime = DateTime . UtcNow ;
2025-01-28 08:54:50 +01:00
return (( m_result . DeletedFileCount + m_result . UploadedFileCount ) > 0 , transaction );
2016-09-15 11:39:27 +02:00
}
else
{
2016-12-01 23:59:54 +01:00
m_result . EndTime = DateTime . UtcNow ;
2025-01-28 08:54:50 +01:00
return ( false , transaction );
2016-09-15 11:39:27 +02:00
}
}
2015-11-17 12:37:33 +01:00
2025-01-28 08:54:50 +01:00
private IEnumerable < KeyValuePair < string , long >> DoDelete ( LocalDeleteDatabase db , IBackendManager backend , IEnumerable < IRemoteVolume > deleteableVolumes , ref System . Data . IDbTransaction transaction , CancellationToken cancellationToken )
2015-11-17 12:37:33 +01:00
{
2024-11-01 14:50:26 +01:00
// Find volumes that can be deleted
var remoteFilesToRemove = db . ReOrderDeleteableVolumes ( deleteableVolumes , transaction ). ToList ();
// Make sure we do not re-assign blocks to any of the volumes we are about to delete
var toRemoveVolumeIds = db . GetRemoteVolumeIDs ( remoteFilesToRemove . Select ( x => x . Name ), transaction )
. Select ( x => x . Value )
. Distinct ()
. ToList ();
2023-06-14 23:42:46 +02:00
// Mark all volumes and relevant index files as disposable
2024-05-21 15:48:29 +02:00
foreach ( var f in remoteFilesToRemove )
2024-11-01 14:50:26 +01:00
{
db . PrepareForDelete ( f . Name , toRemoveVolumeIds , transaction );
2015-11-17 12:37:33 +01:00
db . UpdateRemoteVolume ( f . Name , RemoteVolumeState . Deleting , f . Size , f . Hash , transaction );
2024-11-01 14:50:26 +01:00
}
2015-11-17 12:37:33 +01:00
// Before we commit the current state, make sure the backend has caught up
2025-01-28 08:54:50 +01:00
backend . WaitForEmptyAsync ( db , transaction , cancellationToken ). Await ();
2015-11-17 12:37:33 +01:00
if (! m_options . Dryrun )
{
transaction . Commit ();
transaction = db . BeginTransaction ();
}
2025-01-28 08:54:50 +01:00
return PerformDelete ( backend , remoteFilesToRemove , cancellationToken );
2015-11-17 12:37:33 +01:00
}
2020-01-19 21:57:08 -06:00
2025-01-28 08:54:50 +01:00
private void FinishVolumeAndUpload ( LocalDeleteDatabase db , IBackendManager backendManager , BlockVolumeWriter newvol , IndexVolumeWriter newvolindex , List < KeyValuePair < string , long >> uploadedVolumes )
2020-01-19 21:57:08 -06:00
{
2024-05-21 15:48:29 +02:00
Action indexVolumeFinished = () =>
{
2020-01-19 21:57:08 -06:00
if ( newvolindex != null && m_options . IndexfilePolicy == Options . IndexFileStrategy . Full )
{
foreach ( var blocklist in db . GetBlocklists ( newvol . VolumeID , m_options . Blocksize , m_options . BlockhashSize ))
{
newvolindex . WriteBlocklist ( blocklist . Item1 , blocklist . Item2 , 0 , blocklist . Item3 );
}
}
};
uploadedVolumes . Add ( new KeyValuePair < string , long >( newvol . RemoteFilename , newvol . Filesize ));
if ( newvolindex != null )
uploadedVolumes . Add ( new KeyValuePair < string , long >( newvolindex . RemoteFilename , newvolindex . Filesize ));
if (! m_options . Dryrun )
2025-01-28 08:54:50 +01:00
backendManager . PutAsync ( newvol , newvolindex , indexVolumeFinished , false , CancellationToken . None ). Await ();
2020-01-19 21:57:08 -06:00
else
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldUploadGeneratedBlockset" , "Would upload generated blockset of size {0}" , Library . Utility . Utility . FormatSizeString ( newvol . Filesize ));
}
2025-01-28 08:54:50 +01:00
private IEnumerable < KeyValuePair < string , long >> PerformDelete ( IBackendManager backendManager , IEnumerable < IRemoteVolume > list , CancellationToken cancelToken )
2016-09-15 11:39:27 +02:00
{
2024-05-21 15:48:29 +02:00
foreach ( var f in list )
2016-09-15 11:39:27 +02:00
{
if (! m_options . Dryrun )
2025-01-28 08:54:50 +01:00
backendManager . DeleteAsync ( f . Name , f . Size , false , cancelToken ). Await ();
2016-09-15 11:39:27 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldDeleteRemoteFile" , "Would delete remote file: {0}, size: {1}" , f . Name , Library . Utility . Utility . FormatSizeString ( f . Size ));
2013-04-08 22:24:54 +02:00
2016-09-15 11:39:27 +02:00
yield return new KeyValuePair < string , long >( f . Name , f . Size );
2020-01-19 21:57:08 -06:00
}
2016-09-15 11:39:27 +02:00
}
}
2013-03-27 16:06:45 +01:00
}