mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-27 19:10:29 +08:00
226 lines
No EOL
11 KiB
C#
226 lines
No EOL
11 KiB
C#
// 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
|
|
// 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;
|
|
using NUnit.Framework.Legacy;
|
|
|
|
namespace Duplicati.UnitTest
|
|
{
|
|
public class DeleteHandlerTests : BasicSetupHelper
|
|
{
|
|
private class Fileset : IListResultFileset, IEquatable<Fileset>
|
|
{
|
|
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<IListResultFileset> filesets = new List<IListResultFileset>
|
|
{
|
|
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<string, string> { { "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<string, string> { { "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<string, string> { { "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<string, string> { { "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);
|
|
}
|
|
}
|
|
} |