2019-08-30 12:38:54 -04:00
#region Disclaimer / License
// Copyright (C) 2019, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
2013-03-27 16:06:45 +01:00
//
2019-08-30 12:38:54 -04:00
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
2013-03-27 16:06:45 +01:00
//
2019-08-30 12:38:54 -04:00
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2013-03-27 16:06:45 +01:00
//
2019-08-30 12:38:54 -04:00
#endregion
2013-03-27 16:06:45 +01:00
using System ;
using System.Linq ;
using System.Collections.Generic ;
2019-08-05 20:14:05 -04:00
using System.Globalization ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
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>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < DeleteHandler >();
/// <summary>
/// The tag used for logging retention policy messages
/// </summary>
private static readonly string LOGTAG_RETENTION = LOGTAG + ":RetentionPolicy" ;
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
}
}
2017-09-18 11:54:48 +02:00
public void DoRun ( Database . LocalDeleteDatabase db , ref System . Data . IDbTransaction transaction , bool hasVerifiedBacked , bool forceCompact , BackendManager sharedManager )
{
// 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 ;
2013-05-25 17:32:39 +02:00
if (! hasVerifiedBacked && ! m_options . NoBackendverification )
FilelistProcessor . VerifyRemoteList ( backend , m_options , db , m_result . BackendWriter );
2016-09-15 11:39:27 +02:00
2017-01-05 16:46:01 +01:00
var filesetNumbers = db . FilesetTimes . Zip ( Enumerable . Range ( 0 , db . FilesetTimes . Count ()), ( a , b ) => new Tuple < long , DateTime >( b , a . Value )). ToList ();
2017-01-11 13:51:30 +01:00
var sets = db . FilesetTimes . Select ( x => x . Value ). ToArray ();
2019-08-05 20:14:05 -04:00
var toDelete = GetFilesetsToDelete ( db , sets );
2017-01-11 13:51:30 +01:00
if (! m_options . AllowFullRemoval && sets . Length == toDelete . Length )
{
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" );
2017-01-11 13:51:30 +01:00
toDelete = toDelete . Skip ( 1 ). ToArray ();
}
2013-08-26 15:25:37 +02:00
if ( toDelete != null && toDelete . Length > 0 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DeleteRemoteFileset" , "Deleting {0} remote fileset(s) ..." , toDelete . Length );
2015-11-17 12:37:33 +01:00
var lst = db . DropFilesetsFromTable ( toDelete , transaction ). ToArray ();
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
2013-07-01 17:55:45 +02:00
if (! m_options . NoAutoCompact && ( forceCompact || ( toDelete != null && toDelete . Length > 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
}
2016-09-15 11:39:27 +02:00
2013-08-26 15:25:37 +02:00
m_result . SetResults (
from n in filesetNumbers
where toDelete . Contains ( n . Item2 )
select n ,
m_options . Dryrun );
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
}
2017-09-09 14:08:43 +02:00
/// <summary>
/// Gets the filesets selected for deletion
/// </summary>
/// <returns>The filesets to delete</returns>
2018-01-28 00:23:31 +01:00
/// <param name="allBackups">The list of backups that can be deleted</param>
2019-08-05 20:14:05 -04:00
private DateTime [] GetFilesetsToDelete ( Database . LocalDeleteDatabase db , DateTime [] allBackups )
2017-09-09 14:08:43 +02:00
{
2018-01-28 00:23:31 +01:00
if ( allBackups . Length == 0 )
2019-08-05 20:14:05 -04:00
{
2018-01-28 00:23:31 +01:00
return allBackups ;
2019-08-05 20:14:05 -04:00
}
2019-08-30 12:37:55 -04:00
DateTime [] sortedAllBackups = allBackups . OrderByDescending ( x => x . ToUniversalTime ()). ToArray ();
2019-08-05 20:14:05 -04:00
2019-08-30 12:37:55 -04:00
if ( sortedAllBackups . Select ( x => x . ToUniversalTime ()). Distinct (). Count () != sortedAllBackups . Length )
2019-08-05 20:14:05 -04:00
{
2019-08-30 12:37:55 -04:00
throw new Exception ( $"List of backup timestamps contains duplicates: {string.Join(" , ", sortedAllBackups.Select(x => x.ToString()))}" );
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
2018-01-28 00:23:31 +01:00
List < DateTime > toDelete = new List < DateTime >();
2017-09-09 14:08:43 +02:00
2019-08-05 20:14:05 -04:00
// Remove backups explicitly specified via option
2017-09-09 14:08:43 +02:00
var versions = m_options . Version ;
if ( versions != null && versions . Length > 0 )
2019-08-05 20:14:05 -04:00
{
2017-09-09 14:08:43 +02:00
foreach ( var ix in versions . Distinct ())
2019-08-05 20:14:05 -04:00
{
2019-08-30 12:37:55 -04:00
if ( ix >= 0 && ix < sortedAllBackups . Length )
2019-08-05 20:14:05 -04:00
{
2019-08-30 12:37:55 -04:00
toDelete . Add ( sortedAllBackups [ ix ]);
2019-08-05 20:14:05 -04:00
}
}
}
2017-09-09 14:08:43 +02:00
2018-01-28 00:19:27 +01:00
// Remove backups that are older than date specified via option
2017-09-09 14:08:43 +02:00
var keepTime = m_options . KeepTime ;
if ( keepTime . Ticks > 0 )
2019-08-05 20:14:05 -04:00
{
2019-08-30 12:37:55 -04:00
toDelete . AddRange ( sortedAllBackups . SkipWhile ( x => x >= keepTime ));
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
2019-08-05 20:14:05 -04:00
// Remove backups via retention policy option
2019-08-30 12:37:55 -04:00
toDelete . AddRange ( ApplyRetentionPolicy ( db , sortedAllBackups ));
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-08-30 12:37:55 -04:00
var backupsRemaining = sortedAllBackups . Except ( toDelete ). ToList ();
2019-08-31 16:32:05 -04:00
var fullVersionsToKeep = m_options . KeepVersions ;
var fullVersionsKeptCount = 0 ;
if ( fullVersionsToKeep > 0 && fullVersionsToKeep < backupsRemaining . Count )
2019-08-05 20:14:05 -04:00
{
2019-08-31 16:35:57 -04:00
// keep the number of full backups specified in fullVersionsToKeep.
// once the last full backup t okeep is found, also keep the partials immediately after it the full backup.
2019-08-05 20:14:05 -04:00
// add the remainder of full and partial backups to toDelete
bool foundLastFullBackupToKeep = false ;
foreach ( var backup in backupsRemaining )
{
bool isFullBackup ;
2019-08-31 16:32:05 -04:00
if ( fullVersionsKeptCount < fullVersionsToKeep )
2019-08-05 20:14:05 -04:00
{
isFullBackup = db . IsFilesetFullBackup ( backup );
// count only a full backup
2019-08-31 16:32:05 -04:00
if ( fullVersionsKeptCount < fullVersionsToKeep && isFullBackup )
2019-08-05 20:14:05 -04:00
{
2019-08-31 16:32:05 -04:00
fullVersionsKeptCount ++;
2019-08-05 20:14:05 -04:00
}
continue ;
}
// do not include any partial backup that precedes the last full backup
if (! foundLastFullBackupToKeep )
{
isFullBackup = db . IsFilesetFullBackup ( backup );
if (! isFullBackup )
{
continue ;
}
foundLastFullBackupToKeep = true ;
}
toDelete . Add ( backup );
}
}
2017-09-09 14:08:43 +02:00
2019-08-05 20:14:05 -04:00
var toDeleteDistinct = toDelete . Distinct (). OrderByDescending ( x => x . ToUniversalTime ()). ToArray ();
var removeCount = toDeleteDistinct . Length ;
2019-08-30 12:37:55 -04:00
if ( removeCount > sortedAllBackups . Length )
2019-08-05 20:14:05 -04:00
{
2019-08-30 12:37:55 -04:00
throw new Exception ( $"Too many entries {removeCount} vs {sortedAllBackups.Length}, lists: {string.Join(" , ", toDeleteDistinct.Select(x => x.ToString(CultureInfo.InvariantCulture)))} vs {string.Join(" , ", sortedAllBackups.Select(x => x.ToString(CultureInfo.InvariantCulture)))}" );
2019-08-05 20:14:05 -04:00
}
2017-09-09 14:08:43 +02:00
2019-08-05 20:14:05 -04:00
return toDeleteDistinct ;
2017-09-09 14:08:43 +02:00
}
/// <summary>
2017-09-30 13:14:28 +02:00
/// Deletes backups according to the retention policy configuration.
2017-09-09 14:08:43 +02:00
/// Backups that are not within any of the specified time frames will will NOT be deleted.
/// </summary>
/// <returns>The filesets to delete</returns>
/// <param name="backups">The list of backups that can be deleted</param>
2019-08-05 20:14:05 -04:00
private List < DateTime > ApplyRetentionPolicy ( Database . LocalDeleteDatabase db , DateTime [] backups )
2017-09-09 14:08:43 +02:00
{
// Any work to do?
2018-02-18 16:47:04 +01:00
var retentionPolicyOptionValues = m_options . RetentionPolicy ;
if ( retentionPolicyOptionValues . Count == 0 || backups . Length == 0 )
2017-09-09 14:08:43 +02:00
{
return new List < DateTime >(); // don't delete any backups
}
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
2017-09-30 20:29:54 +02:00
List < DateTime > clonedBackupList = new List < DateTime >( backups );
2017-09-09 14:08:43 +02:00
// Make sure the backups are in descending order (newest backup in the beginning)
2017-09-30 20:29:54 +02:00
clonedBackupList = clonedBackupList . OrderByDescending ( x => x ). ToList ();
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
2018-02-11 15:15:06 +01:00
var mostRecentBackup = clonedBackupList . ElementAt ( 0 );
2017-09-30 20:29:54 +02:00
clonedBackupList . RemoveAt ( 0 );
2018-02-11 15:15:06 +01:00
var deleteMostRecentBackup = m_options . AllowFullRemoval ;
2017-09-09 14:08:43 +02:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "FramesAndIntervals" , "Time frames and intervals pairs: {0}" ,
string . Join ( ", " , retentionPolicyOptionValues ));
2017-09-30 20:29:54 +02:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "BackupList" , "Backups to consider: {0}" ,
string . Join ( ", " , clonedBackupList ));
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.
2017-09-09 14:08:43 +02:00
List < DateTime > backupsToDelete = new List < DateTime >();
2017-09-30 20:29:54 +02:00
var now = DateTime . Now ;
2018-02-18 16:47:04 +01:00
foreach ( var 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
2018-03-12 14:07:11 +01:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "NextTimeAndFrame" , "Next time frame and interval pair: {0}" , singleRetentionPolicyOptionValue . ToString ());
2017-09-09 14:08:43 +02:00
List < DateTime > backupsInTimeFrame = new List < DateTime >();
2017-09-30 20:29:54 +02:00
while ( clonedBackupList . Count > 0 && clonedBackupList [ 0 ] >= 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
}
2018-03-12 14:07:11 +01:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "BackupsInFrame" , "Backups in this time frame: {0}" ,
string . Join ( ", " , backupsInTimeFrame ));
2017-09-09 14:08:43 +02:00
// Run through backups in this time frame
DateTime ? lastKept = null ;
foreach ( DateTime backup in backupsInTimeFrame )
{
2019-08-05 20:14:05 -04:00
var isFullBackup = db . IsFilesetFullBackup ( backup );
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
2018-02-18 16:47:04 +01:00
if ( lastKept == null || singleRetentionPolicyOptionValue . IsKeepAllVersions () || ( backup - lastKept . Value ) >= singleRetentionPolicyOptionValue . Interval )
2017-09-09 14:08:43 +02:00
{
2019-08-05 20:14:05 -04:00
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "KeepBackups" , $"Keeping {(isFullBackup ? "" : " partial ")} backup: {backup}" , Logging . LogMessageType . Profiling );
if ( isFullBackup )
{
lastKept = backup ;
}
2017-09-09 14:08:43 +02:00
}
else
{
2019-08-05 20:14:05 -04:00
if ( isFullBackup )
{
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "DeletingBackups" ,
"Deleting backup: {0}" , backup );
backupsToDelete . Add ( backup );
}
else
{
Logging . Log . WriteProfilingMessage ( LOGTAG_RETENTION , "KeepBackups" , $"Keeping partial backup: {backup}" , Logging . LogMessageType . Profiling );
}
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)
deleteMostRecentBackup &= ( mostRecentBackup < timeFrame );
2017-09-09 14:08:43 +02:00
}
2018-02-11 10:07:37 +01:00
// Delete all remaining backups
backupsToDelete . AddRange ( clonedBackupList );
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "BackupsToDelete" , "Backups outside of all time frames and thus getting deleted: {0}" ,
string . Join ( ", " , clonedBackupList ));
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 )
{
backupsToDelete . Add ( mostRecentBackup );
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "DeleteMostRecent" , "Deleting most recent backup: {0}" ,
mostRecentBackup );
2018-02-11 15:15:06 +01:00
}
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG_RETENTION , "AllBackupsToDelete" , "All backups to delete: {0}" ,
string . Join ( ", " , backupsToDelete . OrderByDescending ( x => x )));
2017-09-09 14:08:43 +02:00
return backupsToDelete ;
}
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
}