2025-01-07 09:40:39 +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.
2017-01-05 16:46:01 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
2017-01-05 16:46:01 +01:00
namespace Duplicati.Library.Main.Operation
{
internal class PurgeBrokenFilesHandler
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( PurgeBrokenFilesHandler ));
2019-10-19 10:56:21 -07:00
protected readonly Options m_options ;
protected readonly PurgeBrokenFilesResults m_result ;
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
public PurgeBrokenFilesHandler ( Options options , PurgeBrokenFilesResults result )
2017-01-05 16:46:01 +01:00
{
m_options = options ;
m_result = result ;
}
2025-01-28 08:54:50 +01:00
public void Run ( IBackendManager backendManager , Library . Utility . IFilter filter )
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" );
2017-01-05 16:46:01 +01:00
if ( filter != null && ! filter . Empty )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Filters are not supported for this operation" , "FiltersNotAllowedOnPurgeBrokenFiles" );
2017-01-05 16:46:01 +01:00
List < Database . RemoteVolumeEntry > missing = null ;
2025-01-28 08:54:50 +01:00
2017-01-05 16:46:01 +01:00
using ( var db = new Database . LocalListBrokenFilesDatabase ( m_options . Dbpath ))
using ( var tr = db . BeginTransaction ())
{
if ( db . PartiallyRecreated )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "The command does not work on partially recreated databases" , "CannotPurgeOnPartialDatabase" );
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
var sets = ListBrokenFilesHandler . GetBrokenFilesetsFromRemote ( backendManager , m_result , db , tr , m_options , out missing );
2017-01-05 16:46:01 +01:00
if ( sets == null )
return ;
2025-01-28 08:54:50 +01:00
2017-01-05 16:46:01 +01:00
if ( sets . Length == 0 )
{
if ( missing == null )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenFilesets" , "Found no broken filesets" );
2017-01-05 16:46:01 +01:00
else if ( missing . Count == 0 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenFilesetsOrMissingFiles" , "Found no broken filesets and no missing remote files" );
2017-01-05 16:46:01 +01:00
else
2018-07-09 20:38:41 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenSetsButMissingRemoteFiles" , string . Format ( "Found no broken filesets, but {0} missing remote files. Purging from database." , missing . Count ));
2017-01-05 16:46:01 +01:00
}
2018-07-09 20:38:41 +02:00
else
2025-01-28 08:54:50 +01:00
{
2018-07-09 20:38:41 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "FoundBrokenFilesets" , "Found {0} broken filesets with {1} affected files, purging files" , sets . Length , sets . Sum ( x => x . Item3 ));
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
var pgoffset = 0.0f ;
var pgspan = 0.95f / sets . Length ;
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
var filesets = db . FilesetTimes . ToList ();
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
var compare_list = sets . Select ( x => new
{
FilesetID = x . Item2 ,
Timestamp = x . Item1 ,
RemoveCount = x . Item3 ,
Version = filesets . FindIndex ( y => y . Key == x . Item2 ),
SetCount = db . GetFilesetFileCount ( x . Item2 , tr )
}). ToArray ();
2017-01-05 16:46:01 +01:00
2020-11-29 14:30:05 -08:00
if ( m_options . Dryrun )
tr . Rollback ();
else
tr . Commit ();
2018-07-09 20:38:41 +02:00
var fully_emptied = compare_list . Where ( x => x . RemoveCount == x . SetCount ). ToArray ();
var to_purge = compare_list . Where ( x => x . RemoveCount != x . SetCount ). ToArray ();
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
if ( fully_emptied . Length != 0 )
2017-01-05 16:46:01 +01:00
{
2018-07-09 20:38:41 +02:00
if ( fully_emptied . Length == 1 )
Logging . Log . WriteInformationMessage ( LOGTAG , "RemovingFilesets" , "Removing entire fileset {1} as all {0} file(s) are broken" , fully_emptied . First (). Timestamp , fully_emptied . First (). RemoveCount );
else
Logging . Log . WriteInformationMessage ( LOGTAG , "RemovingFilesets" , "Removing {0} filesets where all file(s) are broken: {1}" , fully_emptied . Length , string . Join ( ", " , fully_emptied . Select ( x => x . Timestamp . ToLocalTime (). ToString ())));
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
m_result . DeleteResults = new DeleteResults ( m_result );
using ( var rmdb = new Database . LocalDeleteDatabase ( db ))
{
var deltr = rmdb . BeginTransaction ();
try
2017-01-05 16:46:01 +01:00
{
2018-07-09 20:38:41 +02:00
var opts = new Options ( new Dictionary < string , string >( m_options . RawOptions ));
opts . RawOptions [ "version" ] = string . Join ( "," , fully_emptied . Select ( x => x . Version . ToString ()));
opts . RawOptions . Remove ( "time" );
opts . RawOptions [ "no-auto-compact" ] = "true" ;
2025-01-28 08:54:50 +01:00
new DeleteHandler ( opts , ( DeleteResults ) m_result . DeleteResults )
. DoRun ( rmdb , ref deltr , true , false , backendManager );
2018-07-09 20:38:41 +02:00
if (! m_options . Dryrun )
{
using ( new Logging . Timer ( LOGTAG , "CommitDelete" , "CommitDelete" ))
deltr . Commit ();
rmdb . WriteResults ();
}
else
deltr . Rollback ();
2017-01-05 16:46:01 +01:00
}
2018-07-09 20:38:41 +02:00
finally
{
if ( deltr != null )
try { deltr . Rollback (); }
catch { }
}
2017-01-05 16:46:01 +01:00
}
2018-07-09 20:38:41 +02:00
pgoffset += ( pgspan * fully_emptied . Length );
m_result . OperationProgressUpdater . UpdateProgress ( pgoffset );
2017-01-05 16:46:01 +01:00
}
2018-07-09 20:38:41 +02:00
if ( to_purge . Length > 0 )
2017-01-05 16:46:01 +01:00
{
2018-07-09 20:38:41 +02:00
m_result . PurgeResults = new PurgeFilesResults ( m_result );
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
foreach ( var bs in to_purge )
2017-01-05 16:46:01 +01:00
{
2018-07-09 20:38:41 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "PurgingFiles" , "Purging {0} file(s) from fileset {1}" , bs . RemoveCount , bs . Timestamp . ToLocalTime ());
var opts = new Options ( new Dictionary < string , string >( m_options . RawOptions ));
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
using ( var pgdb = new Database . LocalPurgeDatabase ( db ))
2017-01-05 16:46:01 +01:00
{
2018-07-09 20:38:41 +02:00
// Recompute the version number after we deleted the versions before
filesets = pgdb . FilesetTimes . ToList ();
var thisversion = filesets . FindIndex ( y => y . Key == bs . FilesetID );
if ( thisversion < 0 )
throw new Exception ( string . Format ( "Failed to find match for {0} ({1}) in {2}" , bs . FilesetID , bs . Timestamp . ToLocalTime (), string . Join ( ", " , filesets . Select ( x => x . ToString ()))));
opts . RawOptions [ "version" ] = thisversion . ToString ();
opts . RawOptions . Remove ( "time" );
opts . RawOptions [ "no-auto-compact" ] = "true" ;
2025-01-28 08:54:50 +01:00
new PurgeFilesHandler ( opts , ( PurgeFilesResults ) m_result . PurgeResults ). Run ( backendManager , pgdb , pgoffset , pgspan , ( cmd , filesetid , tablename ) =>
2018-07-09 20:38:41 +02:00
{
if ( filesetid != bs . FilesetID )
throw new Exception ( string . Format ( "Unexpected filesetid: {0}, expected {1}" , filesetid , bs . FilesetID ));
db . InsertBrokenFileIDsIntoTable ( filesetid , tablename , "FileID" , cmd . Transaction );
});
}
2017-01-05 16:46:01 +01:00
2018-07-09 20:38:41 +02:00
pgoffset += pgspan ;
m_result . OperationProgressUpdater . UpdateProgress ( pgoffset );
}
2017-01-05 16:46:01 +01:00
}
}
m_result . OperationProgressUpdater . UpdateProgress ( 0.95f );
if (! m_options . Dryrun && db . RepairInProgress )
2025-01-28 08:54:50 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "ValidatingDatabase" , "Database was previously marked as in-progress, checking if it is valid after purging files" );
2017-05-30 00:35:31 +02:00
db . VerifyConsistency ( m_options . Blocksize , m_options . BlockhashSize , true , null );
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "UpdatingDatabase" , "Purge completed, and consistency checks completed, marking database as complete" );
2017-01-05 16:46:01 +01:00
db . RepairInProgress = false ;
}
m_result . OperationProgressUpdater . UpdateProgress ( 1.0f );
}
}
}
}