// 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 System.CommandLine;
using System.CommandLine.NamingConventionBinder;
using System.Text.Json;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Main;
using Duplicati.Library.Main.Database;
using Duplicati.Library.SQLiteHelper;
namespace Duplicati.CommandLine.DatabaseTool.Commands;
///
/// Database status information
///
public class DatabaseStatus
{
///
/// The path to the database
///
public string Path { get; set; } = "";
///
/// The status of the database: Found, Missing, or Orphaned
///
public string Status { get; set; } = "";
///
/// Source of the reference (dbconfig.json, server database, or filesystem)
///
public string Source { get; set; } = "";
///
/// Whether the database file exists on disk
///
public bool FileExists { get; set; }
///
/// Whether the database is referenced in dbconfig.json
///
public bool InDbConfig { get; set; }
///
/// Whether the database is referenced in the server database
///
public bool InServerDb { get; set; }
}
///
/// The verify command - lists all databases with their status
///
public static class Verify
{
///
/// The filename of the file with database configurations
///
private const string CONFIG_FILE = "dbconfig.json";
///
/// Creates the verify command
///
/// The verify command
public static Command Create() =>
new Command("verify", "Verifies database files and shows their status (Found, Missing, Orphaned)")
{
new Option("--datafolder", description: "The folder with databases", getDefaultValue: () => new DirectoryInfo(DataFolderLocator.GetDefaultStorageFolder(DataFolderManager.SERVER_DATABASE_FILENAME, false, true))),
new Option("--output-json", description: "Output as JSON", getDefaultValue: () => false),
new Option("--include-server", description: "Include server database in the list", getDefaultValue: () => true),
}
.WithHandler(CommandHandler.Create(async (datafolder, outputjson, includeserver) =>
{
var datafolderPath = datafolder.FullName;
var results = await AnalyzeDatabasesAsync(datafolderPath, includeserver);
if (outputjson)
{
Console.WriteLine(JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
PrintResults(results, includeserver);
}
}));
///
/// Analyzes all databases and returns their status
///
public static async Task> AnalyzeDatabasesAsync(string datafolder, bool includeServer)
{
var results = new List();
var serverDbPath = Path.Combine(datafolder, DataFolderManager.SERVER_DATABASE_FILENAME);
// Get databases referenced in dbconfig.json
var dbConfigPath = Path.Combine(datafolder, CONFIG_FILE);
var dbConfigPaths = new HashSet(StringComparer.OrdinalIgnoreCase);
if (File.Exists(dbConfigPath))
{
try
{
var configs = Newtonsoft.Json.JsonConvert.DeserializeObject>(
await File.ReadAllTextAsync(dbConfigPath));
if (configs != null)
{
foreach (var config in configs)
{
if (!string.IsNullOrEmpty(config.Databasepath))
dbConfigPaths.Add(Path.GetFullPath(config.Databasepath));
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not parse {CONFIG_FILE}: {ex.Message}");
}
}
// Get databases referenced in server database
var serverDbPaths = new HashSet(StringComparer.OrdinalIgnoreCase);
if (File.Exists(serverDbPath))
{
try
{
await using var con = await SQLiteLoader.LoadConnectionAsync(serverDbPath);
await using var cmd = con.CreateCommand(@"
SELECT ""DBPath""
FROM ""Backup""
");
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(CancellationToken.None))
{
var path = rd.ConvertValueToString(0);
if (!string.IsNullOrEmpty(path))
{
if (!Path.IsPathRooted(path))
path = Path.Combine(datafolder, path);
serverDbPaths.Add(Path.GetFullPath(path));
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not read server database: {ex.Message}");
}
}
// Get all sqlite files in the datafolder
var fileSystemPaths = new HashSet(StringComparer.OrdinalIgnoreCase);
if (Directory.Exists(datafolder))
{
foreach (var file in Directory.EnumerateFiles(datafolder, "*.sqlite", SearchOption.AllDirectories))
{
fileSystemPaths.Add(Path.GetFullPath(file));
}
}
// Combine all unique paths
var allPaths = new HashSet(StringComparer.OrdinalIgnoreCase);
allPaths.UnionWith(dbConfigPaths);
allPaths.UnionWith(serverDbPaths);
allPaths.UnionWith(fileSystemPaths);
// Analyze each path
foreach (var path in allPaths)
{
// Skip server database unless included
if (!includeServer && Path.GetFileName(path).Equals(DataFolderManager.SERVER_DATABASE_FILENAME, StringComparison.OrdinalIgnoreCase))
continue;
var fileExists = File.Exists(path);
var inDbConfig = dbConfigPaths.Contains(path);
var inServerDb = serverDbPaths.Contains(path);
string status;
string source;
if (fileExists && (inDbConfig || inServerDb))
{
status = "Found";
var sources = new List();
if (inDbConfig) sources.Add("dbconfig.json");
if (inServerDb) sources.Add("server");
source = string.Join(", ", sources);
}
else if (!fileExists && (inDbConfig || inServerDb))
{
status = "Missing";
var sources = new List();
if (inDbConfig) sources.Add("dbconfig.json");
if (inServerDb) sources.Add("server");
source = string.Join(", ", sources);
}
else if (fileExists && !inDbConfig && !inServerDb)
{
status = "Orphaned";
source = "filesystem only";
}
else
{
continue; // Should not happen
}
results.Add(new DatabaseStatus
{
Path = path,
Status = status,
Source = source,
FileExists = fileExists,
InDbConfig = inDbConfig,
InServerDb = inServerDb
});
}
return results.OrderBy(r => r.Status).ThenBy(r => r.Path).ToList();
}
///
/// Gets orphaned databases only
///
public static async Task> GetOrphanedDatabasesAsync(string datafolder)
{
var all = await AnalyzeDatabasesAsync(datafolder, true);
return all.Where(d => d.Status == "Orphaned").ToList();
}
///
/// Prints the results to the console
///
private static void PrintResults(List results, bool includeServer)
{
if (results.Count == 0)
{
Console.WriteLine("No databases found.");
return;
}
// Group by status
var found = results.Where(r => r.Status == "Found").ToList();
var missing = results.Where(r => r.Status == "Missing").ToList();
var orphaned = results.Where(r => r.Status == "Orphaned").ToList();
Console.WriteLine($"Database Verification Results");
Console.WriteLine($"==============================");
Console.WriteLine($"Total: {results.Count} databases");
Console.WriteLine($" Found: {found.Count}");
Console.WriteLine($" Missing: {missing.Count}");
Console.WriteLine($" Orphaned: {orphaned.Count}");
Console.WriteLine();
if (found.Count > 0)
{
Console.WriteLine($"Found ({found.Count}):");
Console.WriteLine(new string('-', 80));
foreach (var db in found)
{
Console.WriteLine($" {db.Path}");
Console.WriteLine($" Source: {db.Source}");
}
Console.WriteLine();
}
if (missing.Count > 0)
{
Console.WriteLine($"Missing ({missing.Count}):");
Console.WriteLine(new string('-', 80));
foreach (var db in missing)
{
Console.WriteLine($" {db.Path}");
Console.WriteLine($" Expected in: {db.Source}");
}
Console.WriteLine();
}
if (orphaned.Count > 0)
{
Console.WriteLine($"Orphaned ({orphaned.Count}):");
Console.WriteLine(new string('-', 80));
foreach (var db in orphaned)
{
Console.WriteLine($" {db.Path}");
Console.WriteLine($" Not referenced in dbconfig.json or server database");
}
Console.WriteLine();
}
}
}