Files
duplicati/Duplicati/CommandLine/DatabaseTool/Helper.cs
T
Kenneth Skovhede 179d1ceb47 Make controller Async
This PR has a large blast radius because it takes the final step and bumps up the Controller to be fully async.

We have historically done a piece-by-piece update, so all operations were already async but the controller interface was kept synchronous.

With this update, the controller is now fully async and all tests are updated.

Most places where the new C# compiler warns about function names not ending in `Async` were also adressed, giving a massive refactor change.

Functionally, no changes are done.
2026-05-13 15:04:13 +02:00

206 lines
7.1 KiB
C#

using System.Data;
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;
public static class Helper
{
/// <summary>
/// Creates a backup of the file
/// </summary>
/// <param name="filename">The filename to backup</param>
public static void CreateFileBackup(string path)
{
path = Path.GetFullPath(path);
var dir = Path.GetDirectoryName(path) ?? "";
var filename = Path.GetFileNameWithoutExtension(path);
var newname = $"{filename}-{DateTime.Now:yyyMMddhhmmss}.bak";
var backup = Path.Combine(dir, newname);
var retry = 0;
while (File.Exists(backup))
{
if (retry > 100)
throw new IOException($"Cannot create backup file {backup} - too many retries");
retry++;
newname = $"{filename}-{DateTime.Now:yyyMMddhhmmss}-{retry}.bak";
backup = Path.Combine(dir, newname);
}
File.Copy(path, backup);
}
/// <summary>
/// Find all databases in the data folder
/// </summary>
/// <param name="databases">The input databases</param>
/// <param name="datafolder">The folder to scan</param>
/// <param name="scanExtra">Whether to scan for extra databases</param>
/// <returns>A task that when awaited contains the list of databases.</returns>
public static async Task<string[]> FindAllDatabasesAsync(string[]? databases, string datafolder, bool scanExtra)
{
databases ??= [];
if (databases.Length != 0)
return databases;
// No explicit paths given, so we find all databases in the folder
var serverdb = Path.Combine(datafolder, DataFolderManager.SERVER_DATABASE_FILENAME);
var dbpaths =
CLIDatabaseLocator.GetAllDatabasePaths()
.Prepend(serverdb)
.ToList();
// Append any database paths from the server database
if (File.Exists(serverdb))
{
try
{
await using var con = await SQLiteLoader.LoadConnectionAsync(serverdb)
.ConfigureAwait(false);
await using var cmd = con.CreateCommand(@"
SELECT ""DBPath""
FROM ""Backup""
");
await foreach (var rd in cmd.ExecuteReaderEnumerableAsync(CancellationToken.None).ConfigureAwait(false))
dbpaths.Add(rd.ConvertValueToString(0) ?? "");
}
catch
{
}
}
if (scanExtra)
dbpaths.AddRange(
Directory.EnumerateFiles(datafolder, "*.sqlite", SearchOption.AllDirectories)
.Where(x => CLIDatabaseLocator.IsRandomlyGeneratedName(x))
.Select(x => Path.GetFullPath(x)));
return dbpaths
.Where(x => File.Exists(x))
.Distinct()
.ToArray();
}
/// <summary>
/// Examines the database and returns the version and whether it is a server database
/// </summary>
/// <param name="db">The path to the database</param>
/// <returns>A task that when awaited contains a tuple with the version and whether it is a server database</returns>
public static async Task<(int Version, bool isserver)> ExamineDatabaseAsync(string db)
{
await using var con = await SQLiteLoader.LoadConnectionAsync(db)
.ConfigureAwait(false);
await using var cmd = con.CreateCommand();
var isserverdb = await cmd.ExecuteScalarInt64Async(@"
SELECT COUNT(*)
FROM sqlite_master
WHERE
type='table'
AND name='Backup'
OR name='Schedule'
", CancellationToken.None)
.ConfigureAwait(false) == 2;
var version = (int)await cmd.ExecuteScalarInt64Async(@"
SELECT MAX(Version)
FROM Version
", CancellationToken.None)
.ConfigureAwait(false);
return (version, isserverdb);
}
/// <summary>
/// Prints the data reader to the console
/// </summary>
/// <param name="reader">The data reader to print</param>
/// <param name="useJson">Whether to use JSON or not</param>
public static void Print(this IDataReader reader, bool useJson)
{
if (useJson)
PrintJson(reader);
else
PrintReader(reader);
}
/// <summary>
/// Prints the data reader to the console in JSON format
/// </summary>
/// <param name="reader">The data reader to print</param>
public static void PrintJson(this IDataReader reader)
{
using var writer = new Utf8JsonWriter(Console.OpenStandardOutput(), new JsonWriterOptions { Indented = true });
writer.WriteStartArray();
// Write the header row
writer.WriteStartArray();
for (int i = 0; i < reader.FieldCount; i++)
writer.WriteStringValue(reader.GetName(i));
writer.WriteEndArray();
// Write each data row
while (reader.Read())
{
writer.WriteStartArray();
for (int i = 0; i < reader.FieldCount; i++)
{
var value = reader.GetValue(i);
if (value == DBNull.Value)
writer.WriteNullValue();
else if (value is string s)
writer.WriteStringValue(s);
else if (value is int i32)
writer.WriteNumberValue(i32);
else if (value is long i64)
writer.WriteNumberValue(i64);
else if (value is double d)
writer.WriteNumberValue(d);
else if (value is float f)
writer.WriteNumberValue(f);
else if (value is bool b)
writer.WriteBooleanValue(b);
else if (value is DateTime dt)
writer.WriteStringValue(dt); // ISO 8601 format
else
writer.WriteStringValue(value.ToString()); // Fallback
}
writer.WriteEndArray();
}
writer.WriteEndArray();
writer.Flush();
}
/// <summary>
/// Prints the data reader to the console in a tabular format
/// </summary>
/// <param name="reader">The data reader to print</param>
public static void PrintReader(this IDataReader reader)
{
// Print the header row
for (int i = 0; i < reader.FieldCount; i++)
Console.Write($"{reader.GetName(i)}\t");
Console.WriteLine();
// Print each data row
while (reader.Read())
{
for (int i = 0; i < reader.FieldCount; i++)
{
var value = reader.GetValue(i);
if (value == DBNull.Value)
Console.Write("<null>\t");
else
Console.Write($"{value}\t");
}
Console.WriteLine();
}
}
}