Use effective dbpath for show-log and delete-db endpoints (fixes #1698)
A backup can have two notions of its local database path: the stored Backup.DBPath field and a --dbpath advanced option in its settings. The runner computes an effective path that lets --dbpath override Backup.DBPath, so most operations honor it. But the "Show log", "Show remote log" and "Delete database" endpoints read Backup.DBPath directly, so when --dbpath differs they open/delete the wrong database - "Show log" then fails with "no such table: LogData". Add a shared Runner.GetEffectiveDBPath(IBackup) helper (same precedence as Runner.ApplyOptions) and use it in ExecuteGetLog, ExecuteGetRemotelog and ExecuteDeleteDb so all operations agree on the database file. Adds a unit test for the precedence. The database move/update endpoints are intentionally left unchanged, as they manage the DBPath field itself. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0a31490d15
commit
68dedaa320
@@ -1412,6 +1412,19 @@ namespace Duplicati.Server
|
||||
options["remote-sync-json-config"] = JsonSerializer.Serialize(config, jsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the effective local database path for a backup: the "--dbpath" advanced option if
|
||||
/// it is set, otherwise the stored <see cref="Serialization.Interface.IBackup.DBPath"/>. This
|
||||
/// mirrors the precedence applied by <see cref="ApplyOptions"/> so that every operation agrees
|
||||
/// on which database file to use (see issue #1698).
|
||||
/// </summary>
|
||||
public static string GetEffectiveDBPath(Serialization.Interface.IBackup backup)
|
||||
{
|
||||
var dbpath = backup.Settings?
|
||||
.FirstOrDefault(s => s.Name.Equals("--dbpath", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
return string.IsNullOrWhiteSpace(dbpath) ? backup.DBPath : dbpath;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, string?> ApplyOptions(Connection databaseConnection, Serialization.Interface.IBackup backup, Dictionary<string, string?> options, out string url)
|
||||
{
|
||||
url = backup.TargetURL;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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.Linq;
|
||||
using Duplicati.Server.Database;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
using NUnit.Framework;
|
||||
using Assert = NUnit.Framework.Legacy.ClassicAssert;
|
||||
|
||||
namespace Duplicati.UnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// https://github.com/duplicati/duplicati/issues/1698
|
||||
/// "Show log" and "Database delete" read/deleted the stored <c>Backup.DBPath</c> directly, while
|
||||
/// most operations honor a "--dbpath" advanced option. When those disagreed, Show log opened the
|
||||
/// wrong/empty database ("no such table: LogData"). <see cref="Duplicati.Server.Runner.GetEffectiveDBPath"/>
|
||||
/// reconciles them with the same precedence as the runner; these tests cover that precedence.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Issue1698
|
||||
{
|
||||
private static Backup CreateBackup(string? storedDbPath, params (string Name, string Value)[] settings)
|
||||
{
|
||||
var backup = new Backup
|
||||
{
|
||||
ID = null,
|
||||
Name = "Test Backup",
|
||||
Description = "",
|
||||
Tags = new string[0],
|
||||
TargetURL = "file:///test",
|
||||
Sources = new string[0],
|
||||
Settings = settings.Select(s => (ISetting)new Setting { Name = s.Name, Value = s.Value }).ToArray(),
|
||||
Filters = new IFilter[0],
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
if (storedDbPath != null)
|
||||
backup.SetDBPath(storedDbPath);
|
||||
return backup;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UsesStoredDbPathWhenNoAdvancedOption()
|
||||
{
|
||||
var backup = CreateBackup("/stored/path.sqlite");
|
||||
Assert.AreEqual("/stored/path.sqlite", Duplicati.Server.Runner.GetEffectiveDBPath(backup));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdvancedDbPathOptionOverridesStoredDbPath()
|
||||
{
|
||||
var backup = CreateBackup("/stored/path.sqlite", ("--dbpath", "/override/path.sqlite"));
|
||||
Assert.AreEqual("/override/path.sqlite", Duplicati.Server.Runner.GetEffectiveDBPath(backup));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdvancedDbPathMatchIsCaseInsensitiveOnName()
|
||||
{
|
||||
var backup = CreateBackup("/stored/path.sqlite", ("--DBPath", "/override/path.sqlite"));
|
||||
Assert.AreEqual("/override/path.sqlite", Duplicati.Server.Runner.GetEffectiveDBPath(backup));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BlankAdvancedDbPathFallsBackToStoredDbPath()
|
||||
{
|
||||
var backup = CreateBackup("/stored/path.sqlite", ("--dbpath", " "));
|
||||
Assert.AreEqual("/stored/path.sqlite", Duplicati.Server.Runner.GetEffectiveDBPath(backup));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,20 +187,24 @@ public class BackupGet : IEndpointV1
|
||||
|
||||
private static List<Dictionary<string, object>> ExecuteGetLog(Connection connection, IBackup bk, long? offset, long pagesize)
|
||||
{
|
||||
if (!File.Exists(bk.DBPath))
|
||||
// Use the effective database path (honoring a "--dbpath" advanced option) so that the log is
|
||||
// read from the same database the backup actually uses (see issue #1698).
|
||||
var dbpath = Runner.GetEffectiveDBPath(bk);
|
||||
if (!File.Exists(dbpath))
|
||||
return new List<Dictionary<string, object>>();
|
||||
|
||||
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
|
||||
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(dbpath))
|
||||
using (var cmd = con.CreateCommand())
|
||||
return LogData.DumpTable(cmd, "LogData", "ID", offset, pagesize);
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> ExecuteGetRemotelog(Connection connection, IBackup bk, long? offset, long pagesize)
|
||||
{
|
||||
if (!File.Exists(bk.DBPath))
|
||||
var dbpath = Runner.GetEffectiveDBPath(bk);
|
||||
if (!File.Exists(dbpath))
|
||||
return new List<Dictionary<string, object>>();
|
||||
|
||||
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(bk.DBPath))
|
||||
using (var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection(dbpath))
|
||||
using (var cmd = con.CreateCommand())
|
||||
{
|
||||
var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", offset, pagesize);
|
||||
|
||||
@@ -105,7 +105,9 @@ public class BackupPost : IEndpointV1
|
||||
=> connection.GetBackup(id) ?? throw new NotFoundException("Backup not found");
|
||||
|
||||
private static void ExecuteDeleteDb(IBackup backup)
|
||||
=> File.Delete(backup.DBPath);
|
||||
// Delete the effective database (honoring a "--dbpath" advanced option) so the database the
|
||||
// backup actually uses is removed, not a stale DBPath that may not exist (see issue #1698).
|
||||
=> File.Delete(Runner.GetEffectiveDBPath(backup));
|
||||
|
||||
private static void UpdateDatabasePath(Connection connection, IBackup backup, string targetpath, bool move)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user