mirror of
https://github.com/duplicati/duplicati.git
synced 2026-05-06 07:16:38 -04:00
208 lines
8.4 KiB
C#
208 lines
8.4 KiB
C#
// Copyright (C) 2026, 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 NUnit.Framework;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.UnitTest;
|
|
|
|
public class Issue6705 : BasicSetupHelper
|
|
{
|
|
public static void DeleteDirectoryContentsForcefully(string directoryPath)
|
|
{
|
|
var di = new DirectoryInfo(directoryPath);
|
|
var files = di.GetFiles();
|
|
foreach (FileInfo file in files)
|
|
{
|
|
file.Attributes = FileAttributes.Normal;
|
|
file.Delete();
|
|
}
|
|
var dirs = di.GetDirectories();
|
|
foreach (DirectoryInfo dir in dirs)
|
|
{
|
|
dir.Attributes = FileAttributes.Normal;
|
|
DeleteDirectoryContentsForcefully(dir.FullName);
|
|
dir.Delete();
|
|
}
|
|
}
|
|
|
|
// Copied from CommandLineOperationsTests.cs
|
|
public static async Task DownloadS3FileIfNewerAsync(string destinationFilePath, string url, int retries = 5)
|
|
{
|
|
do
|
|
{
|
|
try
|
|
{
|
|
using var httpClient = new HttpClient(); // Because it's a test unit, will use a new instance created via default constructor.
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
|
|
if (systemIO.FileExists(destinationFilePath))
|
|
request.Headers.IfModifiedSince = systemIO.FileGetLastWriteTimeUtc(destinationFilePath);
|
|
|
|
using var response = await httpClient.SendAsync(request);
|
|
|
|
if (response.StatusCode == System.Net.HttpStatusCode.NotModified)
|
|
{
|
|
Console.WriteLine("File has not been modified since last download.");
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
using var tmpFile = new TempFile();
|
|
Console.WriteLine($"Downloading file from {url} to: {tmpFile}");
|
|
var contentStream = await response.Content.ReadAsStreamAsync();
|
|
var fileInfo = new FileInfo(tmpFile);
|
|
using (var fileStream = fileInfo.OpenWrite())
|
|
await contentStream.CopyToAsync(fileStream);
|
|
|
|
// After download, check if the file length matches the response length
|
|
long responseLength = response.Content.Headers.ContentLength ?? 0;
|
|
long fileLength = new FileInfo(tmpFile).Length;
|
|
if (responseLength != fileLength)
|
|
throw new Exception($"Downloaded file length {fileLength} does not match response length {responseLength}");
|
|
|
|
Console.WriteLine($"Download completed, moving to {destinationFilePath}");
|
|
File.Move(tmpFile, destinationFilePath, true);
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
retries--;
|
|
Console.WriteLine($"Download failed: {ex.Message}");
|
|
if (retries <= 0)
|
|
throw;
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
|
try
|
|
{
|
|
System.Net.Dns.GetHostEntry(new System.Uri(url).Host);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
Console.WriteLine($"Retrying download, {retries} retries left.");
|
|
}
|
|
} while (retries > 0);
|
|
}
|
|
|
|
static string ExtractedBasePath(string os) => Path.Combine(BASEFOLDER, os);
|
|
static string ExtractedPath(string os) => Path.Combine(ExtractedBasePath(os), "cross-os-backup");
|
|
static string ZipPath(string os) => Path.Combine(BASEFOLDER, $"{os}.zip");
|
|
|
|
// The backups were created using the following "generic" script on each OS:
|
|
|
|
/* Small backup:
|
|
git clone git@github.com:duplicati/documentation.git duplicati-docs
|
|
rm -rf duplicati-docs/.gitbook/assets
|
|
dotnet build
|
|
mkdir cross-os-backup
|
|
Executables/Duplicati.CommandLine/bin/Debug/net10.0/Duplicati.CommandLine backup "file://cross-os-backup/backup" ./duplicati-docs --passphrase=123456 --blocksize=10kb --dblock-size=10mb --dbpath="cross-os-backup/db.sqlite"
|
|
mv duplicati-docs cross-os-backup/original
|
|
zip -r {OS}.zip cross-os-backup
|
|
*/
|
|
|
|
/* Large backup:
|
|
git clone git@github.com:duplicati/duplicati.git duplicati
|
|
cd duplicati
|
|
dotnet build
|
|
dotnet build -c Release
|
|
cd ..
|
|
dotnet build
|
|
mkdir cross-os-backup
|
|
Executables/Duplicati.CommandLine/bin/Debug/net10.0/Duplicati.CommandLine backup "file://cross-os-backup/backup" ./duplicati-docs --passphrase=123456 --blocksize=10kb --dblock-size=10mb --dbpath="cross-os-backup/db.sqlite"
|
|
mv duplicati cross-os-backup/original
|
|
zip -r {OS}_large.zip cross-os-backup
|
|
*/
|
|
|
|
[OneTimeSetUp]
|
|
public void DownloadAndExtractBackups()
|
|
{
|
|
bool remote = true; // Set to false to use local files instead of downloading from S3
|
|
var localZipPath = "D:\\git\\duplicati-carl";
|
|
var zipPostfix = ""; // Set to "" for small backup, "_large" for large backup
|
|
foreach (var os in new[] { "Windows", "Linux", "MacOS" })
|
|
{
|
|
string zipFilepath = Path.Combine(localZipPath, $"{os}{zipPostfix}.zip");
|
|
if (remote)
|
|
{
|
|
zipFilepath = ZipPath(os);
|
|
var url = $"https://testfiles.duplicati.com/cross-os-backups/{os}{zipPostfix}.zip";
|
|
DownloadS3FileIfNewerAsync(zipFilepath, url).GetAwaiter().GetResult();
|
|
}
|
|
var extractedPath = ExtractedBasePath(os);
|
|
Directory.CreateDirectory(extractedPath);
|
|
ZipFileExtractToDirectory(zipFilepath, extractedPath);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
[Category("Targeted")]
|
|
public async Task RestoreAcrossOperatingSystems(
|
|
[Values("Windows", "Linux", "MacOS")] string original_os,
|
|
[Values(true, false)] bool use_packed_db,
|
|
[Values(true, false)] bool use_legacy_restore,
|
|
[Values(true, false)] bool skip_metadata
|
|
)
|
|
{
|
|
var extractedPath = ExtractedPath(original_os);
|
|
|
|
var testopts = TestOptions.Expand(new
|
|
{
|
|
// Everything is local at this point, so no need for retries. If there's an issue, fail fast.
|
|
retry_delay = "0",
|
|
number_of_retries = "0",
|
|
|
|
restore_path = RESTOREFOLDER,
|
|
restore_legacy = use_legacy_restore,
|
|
skip_metadata = skip_metadata,
|
|
});
|
|
if (use_packed_db)
|
|
testopts["dbpath"] = Path.Combine(extractedPath, "db.sqlite");
|
|
|
|
using var c = new Library.Main.Controller($"file://{extractedPath}/backup", testopts, null);
|
|
TestUtils.AssertResults(c.Restore(null));
|
|
|
|
// Verify restored files
|
|
TestUtils.AssertDirectoryTreesAreEquivalent(Path.Combine(extractedPath, "original"), RESTOREFOLDER, false, "Restored files do not match original files");
|
|
|
|
DeleteDirectoryContentsForcefully(RESTOREFOLDER);
|
|
}
|
|
|
|
[OneTimeTearDown]
|
|
public void Cleanup()
|
|
{
|
|
foreach (var os in new[] { "Windows", "Linux", "MacOS" })
|
|
{
|
|
if (systemIO.DirectoryExists(ExtractedBasePath(os)))
|
|
systemIO.DirectoryDelete(ExtractedBasePath(os), true);
|
|
if (systemIO.FileExists(ZipPath(os)))
|
|
systemIO.FileDelete(ZipPath(os));
|
|
}
|
|
}
|
|
}
|