Files
duplicati/Duplicati/UnitTest/RelativeDbPathTests.cs
T
Kenneth Skovhede 5c1b477cda Use relative database paths
This PR changes to use relative database paths by default. The default mode is to store all databases in the same folder.

With this update, the paths stored in the server database can now be relative, in which case they are resolved relative to the datafolder.

This makes it simpler to move the data folder as the paths are not stored in full.

For new backups, relative paths are assigned.
For existing backups, the full paths are retained.
If the database path is updated manually, the path will be made relative, if it is relative to the datafolder; otherwise a full path is stored.

This fixes #6677
2026-06-19 08:42:58 +02:00

222 lines
8.2 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.SQLiteHelper;
using Duplicati.Server.Database;
using Duplicati.Server.Serialization.Interface;
using NUnit.Framework;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
/// <summary>
/// Tests for relative DBPath storage and resolution in the server database.
/// </summary>
[TestFixture]
[Category("RelativeDbPath")]
public class RelativeDbPathTests
{
private string _tempDataFolder = null!;
private string _databasePath = null!;
private Connection _connection = null!;
[SetUp]
public async Task SetUpAsync()
{
_tempDataFolder = Path.Combine(Path.GetTempPath(), $"duplicati-relative-db-test-{Guid.NewGuid()}");
Directory.CreateDirectory(_tempDataFolder);
_databasePath = Path.Combine(_tempDataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);
var dbConnection = await SQLiteLoader.LoadConnectionAsync(_databasePath);
DatabaseUpgrader.UpgradeDatabase(dbConnection, _databasePath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
_connection = new Connection(dbConnection, true, null, _tempDataFolder, () => { });
}
[TearDown]
public void TearDown()
{
_connection?.Dispose();
if (Directory.Exists(_tempDataFolder))
{
try
{
Directory.Delete(_tempDataFolder, true);
}
catch
{
// Best effort cleanup
}
}
}
private static Backup CreateTestBackup()
{
return new Backup()
{
ID = null,
Name = "Test Backup",
Description = "Test Description",
Tags = new string[] { "test" },
TargetURL = "file:///test",
Sources = new string[] { "/test" },
Settings = new ISetting[0],
Filters = new IFilter[0],
Metadata = new Dictionary<string, string>()
};
}
[Test]
public void NewBackup_StoresRelativeDbPath()
{
var backup = CreateTestBackup();
_connection.AddOrUpdateBackupAndSchedule(backup, null);
Assert.IsNotNull(backup.DBPath);
Assert.IsFalse(Path.IsPathRooted(backup.DBPath), "New backup DBPath should be stored as relative");
Assert.IsTrue(backup.DBPath.EndsWith(".sqlite", StringComparison.OrdinalIgnoreCase));
}
[Test]
public void LoadBackup_ResolvesRelativeDbPath()
{
var backup = CreateTestBackup();
_connection.AddOrUpdateBackupAndSchedule(backup, null);
var loadedBackup = _connection.GetBackup(backup.ID);
Assert.IsNotNull(loadedBackup);
Assert.IsTrue(Path.IsPathRooted(loadedBackup!.DBPath), "Loaded backup DBPath should be absolute");
Assert.AreEqual(Path.GetFullPath(Path.Combine(_tempDataFolder, backup.DBPath)), loadedBackup.DBPath);
}
[Test]
public void UpdateBackupDbPath_UnderDataFolder_StoresRelative()
{
var backup = CreateTestBackup();
_connection.AddOrUpdateBackupAndSchedule(backup, null);
var newPath = Path.Combine(_tempDataFolder, "moved-backup.sqlite");
File.WriteAllText(newPath, "");
_connection.UpdateBackupDBPath(backup, newPath);
var loadedBackup = _connection.GetBackup(backup.ID);
Assert.IsNotNull(loadedBackup);
// The loaded DBPath should be resolved to absolute
Assert.IsTrue(Path.IsPathRooted(loadedBackup!.DBPath));
Assert.AreEqual(Path.GetFileName(newPath), Path.GetFileName(loadedBackup.DBPath));
// Verify the raw stored value is relative by querying the database directly
using (var db = SQLiteLoader.LoadConnection(_databasePath))
using (var cmd = db.CreateCommand())
{
cmd.CommandText = @"SELECT ""DBPath"" FROM ""Backup"" WHERE ""ID"" = @id";
var param = cmd.CreateParameter();
param.ParameterName = "@id";
param.Value = long.Parse(backup.ID);
cmd.Parameters.Add(param);
var rawDbPath = cmd.ExecuteScalar()?.ToString() ?? "";
Assert.AreEqual("moved-backup.sqlite", rawDbPath);
}
}
[Test]
public void UpdateBackupDbPath_OutsideDataFolder_StoresAbsolute()
{
var backup = CreateTestBackup();
_connection.AddOrUpdateBackupAndSchedule(backup, null);
var outsideFolder = Path.Combine(Path.GetTempPath(), $"duplicati-outside-{Guid.NewGuid()}");
Directory.CreateDirectory(outsideFolder);
try
{
var newPath = Path.Combine(outsideFolder, "moved-backup.sqlite");
File.WriteAllText(newPath, "");
_connection.UpdateBackupDBPath(backup, newPath);
var loadedBackup = _connection.GetBackup(backup.ID);
Assert.IsNotNull(loadedBackup);
Assert.AreEqual(newPath, loadedBackup!.DBPath);
}
finally
{
Directory.Delete(outsideFolder, true);
}
}
[Test]
public void BackupsProperty_ResolvesRelativeDbPaths()
{
var backup1 = CreateTestBackup();
backup1.Name = "Backup 1";
var backup2 = CreateTestBackup();
backup2.Name = "Backup 2";
_connection.AddOrUpdateBackupAndSchedule(backup1, null);
_connection.AddOrUpdateBackupAndSchedule(backup2, null);
var backups = _connection.Backups;
Assert.AreEqual(2, backups.Length);
foreach (var bk in backups)
{
Assert.IsTrue(Path.IsPathRooted(bk.DBPath), $"Backup '{bk.Name}' DBPath should be absolute");
}
}
[Test]
public void ExistingAbsoluteDbPath_StaysAbsolute()
{
var outsideFolder = Path.Combine(Path.GetTempPath(), $"duplicati-outside-{Guid.NewGuid()}");
Directory.CreateDirectory(outsideFolder);
try
{
var absolutePath = Path.Combine(outsideFolder, "external-backup.sqlite");
File.WriteAllText(absolutePath, "");
var backup = CreateTestBackup();
backup.SetDBPath(absolutePath);
_connection.AddOrUpdateBackupAndSchedule(backup, null);
var loadedBackup = _connection.GetBackup(backup.ID);
Assert.IsNotNull(loadedBackup);
Assert.AreEqual(absolutePath, loadedBackup!.DBPath);
}
finally
{
Directory.Delete(outsideFolder, true);
}
}
}
}