From 68dedaa32055704bb3d77594121a9d83ee4e9fa2 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Fri, 10 Jul 2026 22:28:27 +0900 Subject: [PATCH] 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 --- Duplicati/Library/RestAPI/Runner.cs | 13 +++ Duplicati/UnitTest/Issue1698.cs | 91 +++++++++++++++++++ .../Endpoints/V1/Backup/BackupGet.cs | 12 ++- .../Endpoints/V1/Backup/BackupPost.cs | 4 +- 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 Duplicati/UnitTest/Issue1698.cs diff --git a/Duplicati/Library/RestAPI/Runner.cs b/Duplicati/Library/RestAPI/Runner.cs index 8640e8ad6..0eeca7139 100644 --- a/Duplicati/Library/RestAPI/Runner.cs +++ b/Duplicati/Library/RestAPI/Runner.cs @@ -1412,6 +1412,19 @@ namespace Duplicati.Server options["remote-sync-json-config"] = JsonSerializer.Serialize(config, jsonOptions); } + /// + /// Returns the effective local database path for a backup: the "--dbpath" advanced option if + /// it is set, otherwise the stored . This + /// mirrors the precedence applied by so that every operation agrees + /// on which database file to use (see issue #1698). + /// + 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 ApplyOptions(Connection databaseConnection, Serialization.Interface.IBackup backup, Dictionary options, out string url) { url = backup.TargetURL; diff --git a/Duplicati/UnitTest/Issue1698.cs b/Duplicati/UnitTest/Issue1698.cs new file mode 100644 index 000000000..bb571e04f --- /dev/null +++ b/Duplicati/UnitTest/Issue1698.cs @@ -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 +{ + /// + /// https://github.com/duplicati/duplicati/issues/1698 + /// "Show log" and "Database delete" read/deleted the stored Backup.DBPath directly, while + /// most operations honor a "--dbpath" advanced option. When those disagreed, Show log opened the + /// wrong/empty database ("no such table: LogData"). + /// reconciles them with the same precedence as the runner; these tests cover that precedence. + /// + [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() + }; + 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)); + } + } +} diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs index c0f7a5a9e..3228fba0e 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupGet.cs @@ -187,20 +187,24 @@ public class BackupGet : IEndpointV1 private static List> 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>(); - 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> 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>(); - 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); diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPost.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPost.cs index c95cf956f..faa64b78c 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPost.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPost.cs @@ -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) {