// 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. using System; using System.Collections.Generic; using System.Linq; using Duplicati.Library.Interface; using Duplicati.Library.Main; using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Operation; using NUnit.Framework; namespace Duplicati.UnitTest { public class DeleteHandlerTests : BasicSetupHelper { private class Fileset : IListResultFileset, IEquatable { public long Version { get; } public int IsFullBackup { get; } public DateTime Time { get; } public long FileCount { get; } public long FileSizes { get; } public Fileset(int version, int backupType, DateTime time) { this.Version = version; this.IsFullBackup = backupType; this.Time = time; this.FileCount = 0; this.FileSizes = 0; } public bool Equals(Fileset other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return this.Version == other.Version && this.IsFullBackup == other.IsFullBackup && this.Time.Equals(other.Time) && this.FileCount == other.FileCount && this.FileSizes == other.FileSizes; } } [Test] [Category("DeleteHandler")] public void KeepTimeRemover() { DateTime today = DateTime.Today; List filesets = new List { new Fileset(0, BackupType.FULL_BACKUP, today.AddDays(-2)), new Fileset(1, BackupType.FULL_BACKUP, today.AddDays(-1)), new Fileset(2, BackupType.PARTIAL_BACKUP, today), new Fileset(3, BackupType.PARTIAL_BACKUP, today.AddDays(1)), }; // Although version 1 is older than the specified TimeSpan, we do not delete // it because it is the most recent full backup. TimeSpan timeSpan = new TimeSpan(1, 0, 0, 0); IListResultFileset[] expectedFilesetsToRemove = {filesets[0]}; Random random = new Random(); Options options = new Options(new Dictionary {{"keep-time", $"{(int) timeSpan.TotalSeconds}s"}}); KeepTimeRemover remover = new KeepTimeRemover(options); IListResultFileset[] filesetsToRemove = remover.GetFilesetsToDelete(filesets.OrderBy(x => random.Next())).ToArray(); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); // If there is a full backup within the specified TimeSpan, we can respect // the TimeSpan strictly. filesets.Add(new Fileset(4, BackupType.FULL_BACKUP, today.AddDays(2))); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); expectedFilesetsToRemove = filesets.Where(x => x.Time < options.KeepTime).ToArray(); filesetsToRemove = remover.GetFilesetsToDelete(filesets).ToArray(); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); } [Test] [Category("DeleteHandler")] public void KeepVersionsRemover() { IListResultFileset[] filesets = { new Fileset(0, BackupType.FULL_BACKUP, new DateTime(2000, 1, 1)), new Fileset(1, BackupType.PARTIAL_BACKUP, new DateTime(2000, 1, 2)), new Fileset(2, BackupType.FULL_BACKUP, new DateTime(2000, 1, 3)), new Fileset(3, BackupType.PARTIAL_BACKUP, new DateTime(2000, 1, 4)), new Fileset(4, BackupType.FULL_BACKUP, new DateTime(2000, 1, 5)), new Fileset(5, BackupType.PARTIAL_BACKUP, new DateTime(2000, 1, 6)), }; Options options = new Options(new Dictionary {{"keep-versions", "2"}}); IListResultFileset[] expectedFilesetsToRemove = { filesets[0], // Delete; third oldest full backup. filesets[1], // Delete; intermediate partial backup. filesets[3] // Delete; intermediate partial backup. }; Random random = new Random(); KeepVersionsRemover remover = new KeepVersionsRemover(options); IListResultFileset[] filesetsToRemove = remover.GetFilesetsToDelete(filesets.OrderBy(x => random.Next())).ToArray(); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); } [Test] [Category("DeleteHandler")] public void RetentionPolicyRemover() { Options options = new Options(new Dictionary {{"retention-policy", "1W:U,3M:1D,1Y:1W,U:1M"}}); DateTime now = DateTime.Now; IListResultFileset[] filesets = { // Past week. These should all be retained. new Fileset(0, BackupType.PARTIAL_BACKUP, now), new Fileset(1, BackupType.FULL_BACKUP, now.AddMilliseconds(-1)), new Fileset(2, BackupType.FULL_BACKUP, now.AddSeconds(-1)), new Fileset(3, BackupType.PARTIAL_BACKUP, now.AddMinutes(-1)), new Fileset(4, BackupType.PARTIAL_BACKUP, now.AddHours(-1)), new Fileset(5, BackupType.PARTIAL_BACKUP, now.AddDays(-1)), new Fileset(6, BackupType.FULL_BACKUP, now.AddDays(-6)), // Past 3 months. new Fileset(7, BackupType.FULL_BACKUP, now.AddDays(-8)), // Keep; first in interval. new Fileset(8, BackupType.FULL_BACKUP, now.AddDays(-8).AddHours(1)), // Delete; second in interval. new Fileset(9, BackupType.FULL_BACKUP, now.AddDays(-8).AddHours(2)), // Delete; third in interval new Fileset(10, BackupType.PARTIAL_BACKUP, now.AddMonths(-1)), // Keep; partial new Fileset(11, BackupType.PARTIAL_BACKUP, now.AddMonths(-1).AddHours(1)), // Keep; partial new Fileset(12, BackupType.FULL_BACKUP, now.AddMonths(-1).AddHours(2)), // Keep; first full in interval. Do not discard full in favor of partial. new Fileset(13, BackupType.FULL_BACKUP, now.AddMonths(-2)), // Keep; first in interval new Fileset(14, BackupType.FULL_BACKUP, now.AddMonths(-2).AddHours(1)), // Delete; second in interval. new Fileset(15, BackupType.FULL_BACKUP, now.AddDays(-89).AddHours(1)), // Keep; first in interval. new Fileset(16, BackupType.FULL_BACKUP, now.AddDays(-89).AddHours(2)), // Delete; second in interval. // Past year. new Fileset(17, BackupType.FULL_BACKUP, now.AddDays(-92)), // Keep; first in interval. new Fileset(18, BackupType.FULL_BACKUP, now.AddDays(-91)), // Delete; second in interval. new Fileset(19, BackupType.PARTIAL_BACKUP, now.AddDays(-(90 + 73))), // Keep; partial new Fileset(20, BackupType.PARTIAL_BACKUP, now.AddDays(-(90 + 72))), // Keep; partial. new Fileset(21, BackupType.PARTIAL_BACKUP, now.AddDays(-(90 + 71))), // Keep; first full in interval. Do not discard full in favor of partial. new Fileset(22, BackupType.FULL_BACKUP, now.AddDays(-(90 + 142))), // Keep; first in interval. new Fileset(23, BackupType.FULL_BACKUP, now.AddDays(-(90 + 141))), // Delete; second in interval. // Unlimited. new Fileset(24, BackupType.FULL_BACKUP, now.AddYears(-1).AddMonths(-1)), // Keep; first in interval. new Fileset(25, BackupType.FULL_BACKUP, now.AddYears(-1).AddMonths(-1).AddDays(1)), // Delete; second in interval. new Fileset(26, BackupType.PARTIAL_BACKUP, new DateTime(1, 1, 1)), // Keep; partial new Fileset(27, BackupType.FULL_BACKUP, new DateTime(1, 1, 30)), // Keep; first full in interval. Do not discard full in favor of partial. }; IListResultFileset[] expectedFilesetsToRemove = { // 3M:1D filesets[8], filesets[9], filesets[14], filesets[16], // 1Y:1W filesets[18], filesets[23], // U:1M filesets[25], }; Random random = new Random(); RetentionPolicyRemover remover = new RetentionPolicyRemover(options); IListResultFileset[] filesetsToRemove = remover.GetFilesetsToDelete(filesets.OrderBy(x => random.Next())).ToArray(); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); } [Test] [Category("DeleteHandler")] public void SpecificVersionsRemover() { IListResultFileset[] filesets = { new Fileset(0, BackupType.FULL_BACKUP, new DateTime(2000, 1, 1)), new Fileset(1, BackupType.PARTIAL_BACKUP, new DateTime(2000, 1, 2)), new Fileset(2, BackupType.FULL_BACKUP, new DateTime(2000, 1, 3)), new Fileset(3, BackupType.PARTIAL_BACKUP, new DateTime(2000, 1, 4)), new Fileset(4, BackupType.FULL_BACKUP, new DateTime(2000, 1, 5)) }; Options options = new Options(new Dictionary {{"version", "0,3,4"}}); IListResultFileset[] expectedFilesetsToRemove = { filesets[0], filesets[3], filesets[4] }; Random random = new Random(); SpecificVersionsRemover remover = new SpecificVersionsRemover(options); IListResultFileset[] filesetsToRemove = remover.GetFilesetsToDelete(filesets.OrderBy(x => random.Next())).ToArray(); CollectionAssert.AreEquivalent(expectedFilesetsToRemove, filesetsToRemove); } } }