2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, 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
// DEALINGS IN THE SOFTWARE.
2013-03-27 16:06:45 +01:00
using System ;
using System.Linq ;
using System.Collections.Generic ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
2019-12-09 20:31:15 -08:00
using Duplicati.Library.Main.Database ;
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 DeleteHandler
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
2019-12-09 20:31:15 -08:00
internal static readonly string LOGTAG = Logging . Log . LogTagFromType < DeleteHandler >();
2018-03-12 14:07:11 +01:00
2018-05-23 21:18:01 -07:00
private readonly DeleteResults m_result ;
2019-10-19 10:56:21 -07:00
protected readonly string m_backendurl ;
protected readonly Options m_options ;
2013-05-25 16:40:15 +02:00
2016-09-15 11:39:27 +02:00
public DeleteHandler ( string backend , Options options , DeleteResults result )
2013-03-27 16:06:45 +01:00
{
2013-05-25 16:40:15 +02:00
m_backendurl = backend ;
m_options = options ;
m_result = result ;
2013-03-27 16:06:45 +01:00
}
2019-08-05 20:14:05 -04:00
2013-05-25 16:40:15 +02:00
public void Run ()
2013-05-25 17:32:39 +02:00
{
2013-05-25 16:40:15 +02: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 ), "DatabaseFileMissing" );
2013-03-27 16:06:45 +01:00
2016-12-29 22:41:13 +01:00
using ( var db = new Database . LocalDeleteDatabase ( m_options . Dbpath , "Delete" ))
2013-05-25 16:40:15 +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 );
Utility . VerifyParameters ( db , m_options );
2017-09-18 11:54:48 +02:00
DoRun ( db , ref tr , false , false , null );
2015-11-17 12:37:33 +01:00
if (! m_options . Dryrun )
{
2018-03-12 14:07:11 +01:00
using ( new Logging . Timer ( LOGTAG , "CommitDelete" , "CommitDelete" ))
2015-11-17 12:37:33 +01:00
tr . Commit ();
2014-07-16 00:57:57 +02:00
2015-11-17 12:37:33 +01:00
db . WriteResults ();
}
else
tr . Rollback ();
tr = null ;
}
finally
{
if ( tr != null )
try { tr . Rollback (); }
catch { }
2013-06-09 16:26:08 +02:00
}
2013-05-25 17:32:39 +02:00
}
}
2020-04-05 11:43:08 -07:00
public void DoRun ( Database . LocalDeleteDatabase db , ref System . Data . IDbTransaction transaction , bool hasVerifiedBackend , bool forceCompact , BackendManager sharedManager )
2017-09-18 11:54:48 +02:00
{
// Workaround where we allow a running backendmanager to be used
using ( var bk = sharedManager == null ? new BackendManager ( m_backendurl , m_options , m_result . BackendWriter , db ) : null )
2013-05-25 17:32:39 +02:00
{
2017-09-18 11:54:48 +02:00
var backend = bk ?? sharedManager ;
2020-04-05 11:43:08 -07:00
if (! hasVerifiedBackend )
2020-02-29 16:33:32 -06:00
{
2020-04-05 11:41:48 -07:00
FilelistProcessor . VerifyRemoteList ( backend , m_options , db , m_result . BackendWriter , true , transaction );
2020-02-29 16:33:32 -06:00
}
2019-12-09 20:31:15 -08:00
2019-12-12 18:38:17 -08:00
IListResultFileset [] filesets = db . FilesetsWithBackupVersion . ToArray ();
2019-12-09 20:31:15 -08:00
List < IListResultFileset > versionsToDelete = new List < IListResultFileset >();
versionsToDelete . AddRange ( new SpecificVersionsRemover ( this . m_options ). GetFilesetsToDelete ( filesets ));
versionsToDelete . AddRange ( new KeepTimeRemover ( this . m_options ). GetFilesetsToDelete ( filesets ));
versionsToDelete . AddRange ( new RetentionPolicyRemover ( this . m_options ). GetFilesetsToDelete ( filesets ));
2017-01-11 13:51:30 +01:00
2019-12-09 20:31:15 -08:00
// When determining the number of full versions to keep, we need to ignore the versions already marked for removal.
versionsToDelete . AddRange ( new KeepVersionsRemover ( this . m_options ). GetFilesetsToDelete ( filesets . Except ( versionsToDelete )));
2019-12-15 13:51:08 -08:00
if (! m_options . AllowFullRemoval && filesets . Length == versionsToDelete . Count )
2017-01-11 13:51:30 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "PreventingLastFilesetRemoval" , "Preventing removal of last fileset, use --{0} to allow removal ..." , "allow-full-removal" );
2019-12-15 13:51:08 -08:00
versionsToDelete = versionsToDelete . OrderBy ( x => x . Version ). Skip ( 1 ). ToList ();
2017-01-11 13:51:30 +01:00
}
2019-12-15 13:51:08 -08:00
if ( versionsToDelete . Count > 0 )
Logging . Log . WriteInformationMessage ( LOGTAG , "DeleteRemoteFileset" , "Deleting {0} remote fileset(s) ..." , versionsToDelete . Count );
2015-11-17 12:37:33 +01:00
2019-12-15 13:51:08 -08:00
var lst = db . DropFilesetsFromTable ( versionsToDelete . Select ( x => x . Time ). ToArray (), transaction ). ToArray ();
2015-11-17 12:37:33 +01:00
foreach ( var f in lst )
db . UpdateRemoteVolume ( f . Key , RemoteVolumeState . Deleting , f . Value , null , transaction );
if (! m_options . Dryrun )
{
transaction . Commit ();
transaction = db . BeginTransaction ();
}
foreach ( var f in lst )
2013-05-25 16:40:15 +02:00
{
2014-05-15 12:47:16 +02:00
if ( m_result . TaskControlRendevouz () == TaskControlState . Stop )
{
backend . WaitForComplete ( db , transaction );
return ;
2015-11-17 12:37:33 +01:00
}
2013-05-25 16:56:53 +02:00
if (! m_options . Dryrun )
2013-08-25 07:49:31 +02:00
backend . Delete ( f . Key , f . Value );
2013-05-25 16:40:15 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldDeleteRemoteFileset" , "Would delete remote fileset: {0}" , f . Key );
2013-05-25 17:32:39 +02:00
}
2017-09-26 22:45:24 +02:00
if ( sharedManager == null )
backend . WaitForComplete ( db , transaction );
else
backend . WaitForEmpty ( db , transaction );
2016-09-15 11:39:27 +02:00
2015-11-17 12:37:33 +01:00
var count = lst . Length ;
2013-05-25 17:32:39 +02:00
if (! m_options . Dryrun )
{
if ( count == 0 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DeleteResults" , "No remote filesets were deleted" );
2013-05-25 17:32:39 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DeleteResults" , "Deleted {0} remote fileset(s)" , count );
2013-05-25 17:32:39 +02:00
}
else
{
2016-09-15 11:39:27 +02:00
2013-05-25 17:32:39 +02:00
if ( count == 0 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldDeleteResults" , "No remote filesets would be deleted" );
2013-05-25 17:32:39 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldDeleteResults" , "{0} remote fileset(s) would be deleted" , count );
2013-03-27 16:06:45 +01:00
2013-05-25 17:32:39 +02:00
if ( count > 0 && m_options . Dryrun )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteDryrunMessage ( LOGTAG , "WouldDeleteHelp" , "Remove --dry-run to actually delete files" );
2013-05-25 17:32:39 +02:00
}
2016-09-15 11:39:27 +02:00
2019-12-15 13:51:08 -08:00
if (! m_options . NoAutoCompact && ( forceCompact || versionsToDelete . Count > 0 ))
2013-05-25 17:32:39 +02:00
{
m_result . CompactResults = new CompactResults ( m_result );
2017-09-18 11:54:48 +02:00
new CompactHandler ( m_backendurl , m_options , ( CompactResults ) m_result . CompactResults ). DoCompact ( db , true , ref transaction , sharedManager );
2013-05-25 17:32:39 +02:00
}
2019-12-09 20:31:15 -08:00
m_result . SetResults ( versionsToDelete . Select ( v => new Tuple < long , DateTime >( v . Version , v . Time )), m_options . Dryrun );
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
}
2019-12-09 20:31:15 -08:00
}
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
public abstract class FilesetRemover
{
protected readonly Options Options ;
2019-08-05 20:14:05 -04:00
2019-12-09 20:31:15 -08:00
protected FilesetRemover ( Options options )
{
this . Options = options ;
}
2019-08-05 20:14:05 -04:00
2019-12-09 20:31:15 -08:00
public abstract IEnumerable < IListResultFileset > GetFilesetsToDelete ( IEnumerable < IListResultFileset > filesets );
}
2017-09-09 14:08:43 +02:00
2019-12-11 20:43:34 -08:00
/// <summary>
/// Remove versions specified by the --version option.
/// </summary>
public class SpecificVersionsRemover : FilesetRemover
{
public SpecificVersionsRemover ( Options options ) : base ( options )
{
}
public override IEnumerable < IListResultFileset > GetFilesetsToDelete ( IEnumerable < IListResultFileset > filesets )
{
ISet < long > versionsToDelete = new HashSet < long >( this . Options . Version ?? new long [ 0 ]);
return filesets . Where ( x => versionsToDelete . Contains ( x . Version ));
}
}
2019-12-09 20:31:15 -08:00
/// <summary>
/// Keep backups that are newer than the date specified by the --keep-time option.
/// If none of the retained versions are full backups, then continue to keep versions
/// until we have a full backup.
/// </summary>
public class KeepTimeRemover : FilesetRemover
{
public KeepTimeRemover ( Options options ) : base ( options )
{
}
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
public override IEnumerable < IListResultFileset > GetFilesetsToDelete ( IEnumerable < IListResultFileset > filesets )
{
IListResultFileset [] sortedFilesets = filesets . OrderByDescending ( x => x . Time ). ToArray ();
List < IListResultFileset > versionsToDelete = new List < IListResultFileset >();
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
DateTime earliestTime = this . Options . KeepTime ;
if ( earliestTime . Ticks > 0 )
2019-08-05 20:14:05 -04:00
{
2019-11-23 11:24:33 -08:00
bool haveFullBackup = false ;
2019-12-09 20:31:15 -08:00
versionsToDelete . AddRange ( sortedFilesets . SkipWhile ( x =>
2019-11-23 11:24:33 -08:00
{
2019-12-09 20:31:15 -08:00
bool keepBackup = ( x . Time >= earliestTime ) || ! haveFullBackup ;
haveFullBackup = haveFullBackup || ( x . IsFullBackup == BackupType . FULL_BACKUP );
2019-11-23 11:24:33 -08:00
return keepBackup ;
}));
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
return versionsToDelete ;
}
}
/// <summary>
/// Keep a number of recent full backups as specified by the --keep-versions option.
/// Partial backups that are surrounded by full backups will also be removed.
/// </summary>
public class KeepVersionsRemover : FilesetRemover
{
public KeepVersionsRemover ( Options options ) : base ( options )
{
}
public override IEnumerable < IListResultFileset > GetFilesetsToDelete ( IEnumerable < IListResultFileset > filesets )
{
IListResultFileset [] sortedFilesets = filesets . OrderByDescending ( x => x . Time ). ToArray ();
List < IListResultFileset > versionsToDelete = new List < IListResultFileset >();
2017-09-09 14:08:43 +02:00
2019-08-05 20:14:05 -04:00
// Check how many full backups will be remaining after the previous steps
2018-01-28 00:19:27 +01:00
// and remove oldest backups while there are still more backups than should be kept as specified via option
2019-12-09 20:31:15 -08:00
int fullVersionsToKeep = this . Options . KeepVersions ;
if ( fullVersionsToKeep > 0 && fullVersionsToKeep < sortedFilesets . Length )
2019-08-05 20:14:05 -04:00
{
2019-11-29 19:04:43 -08:00
int fullVersionsKept = 0 ;
2019-12-09 20:31:15 -08:00
ISet < IListResultFileset > intermediatePartials = new HashSet < IListResultFileset >();
2019-11-18 20:10:56 -08:00
2019-11-29 19:04:43 -08:00
// Enumerate the collection starting from the most recent full backup.
2019-12-09 20:31:15 -08:00
foreach ( IListResultFileset fileset in sortedFilesets . SkipWhile ( x => x . IsFullBackup == BackupType . PARTIAL_BACKUP ))
2019-08-05 20:14:05 -04:00
{
2019-11-29 19:04:43 -08:00
if ( fullVersionsKept >= fullVersionsToKeep )
2019-11-18 20:10:56 -08:00
{
2019-11-29 19:04:43 -08:00
// If we have enough full backups, delete all older backups.
2019-12-09 20:31:15 -08:00
versionsToDelete . Add ( fileset );
2019-11-18 20:10:56 -08:00
}
2019-12-09 20:31:15 -08:00
else if ( fileset . IsFullBackup == BackupType . FULL_BACKUP )
2019-08-05 20:14:05 -04:00
{
2019-11-29 19:04:43 -08:00
// We can delete partial backups that are surrounded by full backups.
2019-12-09 20:31:15 -08:00
versionsToDelete . AddRange ( intermediatePartials );
2019-11-29 19:04:43 -08:00
intermediatePartials . Clear ();
fullVersionsKept ++;
2019-08-05 20:14:05 -04:00
}
2019-11-18 20:10:56 -08:00
else
2019-08-05 20:14:05 -04:00
{
2019-12-09 20:31:15 -08:00
intermediatePartials . Add ( fileset );
2019-08-05 20:14:05 -04:00
}
}
}
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
return versionsToDelete ;
}
}
/// <summary>
/// Remove backups according to the --retention-policy option.
/// Partial backups are not removed.
/// </summary>
public class RetentionPolicyRemover : FilesetRemover
{
private static readonly string LOGTAG_RETENTION = DeleteHandler . LOGTAG + ":RetentionPolicy" ;
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
public RetentionPolicyRemover ( Options options ) : base ( options )
{
2017-09-09 14:08:43 +02:00
}
2019-12-09 20:31:15 -08:00
public override IEnumerable < IListResultFileset > GetFilesetsToDelete ( IEnumerable < IListResultFileset > filesets )
2017-09-09 14:08:43 +02:00
{
2019-12-09 20:31:15 -08:00
IListResultFileset [] sortedFilesets = filesets . OrderByDescending ( x => x . Time ). ToArray ();
List < IListResultFileset > versionsToDelete = new List < IListResultFileset >();
List < Options . RetentionPolicyValue > retentionPolicyOptionValues = this . Options . RetentionPolicy ;
if ( retentionPolicyOptionValues . Count == 0 || sortedFilesets . Length == 0 )
2017-09-09 14:08:43 +02:00
{
2019-12-09 20:31:15 -08:00
return versionsToDelete ;
2017-09-09 14:08:43 +02:00
}
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "StartCheck" , "Start checking if backups can be removed" );
2017-09-09 14:08:43 +02:00
// Work with a copy to not modify the enumeration that the caller passed
2019-12-09 20:31:15 -08:00
List < IListResultFileset > clonedBackupList = new List < IListResultFileset >( sortedFilesets );
2017-09-09 14:08:43 +02:00
2018-02-11 15:15:06 +01:00
// Most recent backup usually should never get deleted in this process, so exclude it for now,
2019-08-05 20:14:05 -04:00
// but keep a reference to potential delete it when allow-full-removal is set
2019-12-09 20:31:15 -08:00
IListResultFileset mostRecentBackup = clonedBackupList . ElementAt ( 0 );
2017-09-30 20:29:54 +02:00
clonedBackupList . RemoveAt ( 0 );
2019-12-09 20:31:15 -08:00
bool deleteMostRecentBackup = this . Options . AllowFullRemoval ;
2017-09-30 20:29:54 +02:00
2019-12-09 20:31:15 -08:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "FramesAndIntervals" , "Time frames and intervals pairs: {0}" , string . Join ( ", " , retentionPolicyOptionValues ));
2020-02-18 17:42:36 -08:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "BackupList" , "Backups to consider: {0}" , string . Join ( ", " , clonedBackupList . Select ( x => x . Time )));
2017-09-09 14:08:43 +02:00
2017-09-30 20:29:54 +02:00
// Collect all potential backups in each time frame and thin out according to the specified interval,
// starting with the oldest backup in that time frame.
// The order in which the time frames values are checked has to be from the smallest to the largest.
2019-12-09 20:31:15 -08:00
DateTime now = DateTime . Now ;
foreach ( Options . RetentionPolicyValue singleRetentionPolicyOptionValue in retentionPolicyOptionValues . OrderBy ( x => x . Timeframe ))
2017-09-09 14:08:43 +02:00
{
2018-02-18 16:47:04 +01:00
// The timeframe in the retention policy option is only a timespan which has to be applied to the current DateTime to get the actual lower bound
DateTime timeFrame = ( singleRetentionPolicyOptionValue . IsUnlimtedTimeframe ()) ? DateTime . MinValue : ( now - singleRetentionPolicyOptionValue . Timeframe );
2017-09-30 20:29:54 +02:00
2020-02-18 17:45:10 -08:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "NextTimeAndFrame" , "Next time frame and interval pair: {0}" , singleRetentionPolicyOptionValue );
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
List < IListResultFileset > backupsInTimeFrame = new List < IListResultFileset >();
while ( clonedBackupList . Count > 0 && clonedBackupList [ 0 ]. Time >= timeFrame )
2017-09-09 14:08:43 +02:00
{
2019-08-05 20:14:05 -04:00
backupsInTimeFrame . Insert ( 0 , clonedBackupList [ 0 ]); // Insert at beginning to reverse order, which is necessary for next step
2017-09-30 20:29:54 +02:00
clonedBackupList . RemoveAt ( 0 ); // remove from here to not handle the same backup in two time frames
2017-09-09 14:08:43 +02:00
}
2020-02-18 17:42:36 -08:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "BackupsInFrame" , "Backups in this time frame: {0}" , string . Join ( ", " , backupsInTimeFrame . Select ( x => x . Time )));
2017-09-09 14:08:43 +02:00
// Run through backups in this time frame
2019-12-09 20:31:15 -08:00
IListResultFileset lastKept = null ;
foreach ( IListResultFileset fileset in backupsInTimeFrame )
2017-09-09 14:08:43 +02:00
{
2019-12-09 20:31:15 -08:00
bool isFullBackup = fileset . IsFullBackup == BackupType . FULL_BACKUP ;
2019-08-05 20:14:05 -04:00
2017-09-09 14:08:43 +02:00
// Keep this backup if
// - no backup has yet been added to the time frame (keeps at least the oldest backup in a time frame)
// - difference between last added backup and this backup is bigger than the specified interval
2019-12-09 20:31:15 -08:00
if ( lastKept == null || singleRetentionPolicyOptionValue . IsKeepAllVersions () || ( fileset . Time - lastKept . Time ) >= singleRetentionPolicyOptionValue . Interval )
2017-09-09 14:08:43 +02:00
{
2020-02-18 17:42:36 -08:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "KeepBackups" , $"Keeping {(isFullBackup ? "" : " partial ")} backup: {fileset.Time}" , Logging . LogMessageType . Profiling );
2019-08-05 20:14:05 -04:00
if ( isFullBackup )
{
2019-12-09 20:31:15 -08:00
lastKept = fileset ;
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
}
else
{
2019-08-05 20:14:05 -04:00
if ( isFullBackup )
{
2020-02-18 17:42:36 -08:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "DeletingBackups" , "Deleting backup: {0}" , fileset . Time );
2019-12-09 20:31:15 -08:00
versionsToDelete . Add ( fileset );
2019-08-05 20:14:05 -04:00
}
else
{
2020-02-18 17:42:36 -08:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "KeepBackups" , $"Keeping partial backup: {fileset.Time}" , Logging . LogMessageType . Profiling );
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
}
}
2018-02-11 15:15:06 +01:00
// Check if most recent backup is outside of this time frame (meaning older/smaller)
2019-12-09 20:31:15 -08:00
deleteMostRecentBackup &= ( mostRecentBackup . Time < timeFrame );
2017-09-09 14:08:43 +02:00
}
2018-02-11 10:07:37 +01:00
// Delete all remaining backups
2019-12-09 20:31:15 -08:00
versionsToDelete . AddRange ( clonedBackupList );
2020-02-18 17:42:36 -08:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "BackupsToDelete" , "Backups outside of all time frames and thus getting deleted: {0}" , string . Join ( ", " , clonedBackupList . Select ( x => x . Time )));
2017-09-09 14:08:43 +02:00
2018-02-11 15:15:06 +01:00
// Delete most recent backup if allow-full-removal is set and the most current backup is outside of any time frame
if ( deleteMostRecentBackup )
{
2019-12-09 20:31:15 -08:00
versionsToDelete . Add ( mostRecentBackup );
2020-02-18 17:42:36 -08:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "DeleteMostRecent" , "Deleting most recent backup: {0}" , mostRecentBackup . Time );
2018-02-11 15:15:06 +01:00
}
2020-02-18 17:42:36 -08:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "AllBackupsToDelete" , "All backups to delete: {0}" , string . Join ( ", " , versionsToDelete . Select ( x => x . Time ). OrderByDescending ( x => x )));
2017-09-09 14:08:43 +02:00
2019-12-09 20:31:15 -08:00
return versionsToDelete ;
}
}
2013-03-27 16:06:45 +01:00
}