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:
JamBalaya56562
2026-07-10 22:28:27 +09:00
co-authored by Claude Opus 4.8
parent 0a31490d15
commit 68dedaa320
4 changed files with 115 additions and 5 deletions
+13
View File
@@ -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;
+91
View File
@@ -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)
{