2025-04-24 16:36:14 +02:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-06-11 08:56:58 +02:00
//
// 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
2025-04-24 16:36:14 +02:00
// Software is furnished to do so, subject to the following conditions:
2025-06-11 08:56:58 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-04-24 16:36:14 +02:00
// all copies or substantial portions of the Software.
2025-06-11 08:56:58 +02:00
//
// 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
2025-04-24 16:36:14 +02:00
// DEALINGS IN THE SOFTWARE.
using System.CommandLine ;
using System.CommandLine.NamingConventionBinder ;
using Duplicati.Library.AutoUpdater ;
using Duplicati.Library.Interface ;
using Duplicati.Library.Main.Database ;
using Duplicati.Library.SQLiteHelper ;
2025-06-11 08:57:14 +02:00
using Duplicati.Library.Utility ;
2025-04-24 16:36:14 +02:00
namespace Duplicati.CommandLine.DatabaseTool.Commands ;
/// <summary>
/// The downgrade command
/// </summary>
public static class Downgrade
{
/// <summary>
/// Creates the downgrade command
/// </summary>
/// <returns>The downgrade command</returns>
public static Command Create () =>
new Command ( "downgrade" , "Downgrades one or more databases to a prior version" )
{
new Argument < string []>( "databases" , "The databases to downgrade" ) {
Arity = ArgumentArity . ZeroOrMore
},
new Option < DirectoryInfo >( "--server-datafolder" , description : "The folder with databases" , getDefaultValue : () => new DirectoryInfo ( DataFolderLocator . GetDefaultStorageFolder ( DataFolderManager . SERVER_DATABASE_FILENAME , false , true ))),
2026-01-30 13:59:47 +01:00
new Option < int >( "--server-version" , description : "The version to downgrade the server database to" , getDefaultValue : () => 9 ),
2025-04-25 13:44:57 +02:00
new Option < int >( "--local-version" , description : "The version to downgrade local databases to" , getDefaultValue : () => 14 ),
2025-04-24 16:36:14 +02:00
new Option < bool >( "--no-backups" , description : "Do not create backups before downgrade" , getDefaultValue : () => false ),
2025-04-25 11:13:50 +02:00
new Option < bool >( "--include-untracked-databases" , description : "Include untracked databases in the downgrade process" , getDefaultValue : () => false )
2025-04-24 16:36:14 +02:00
}
2025-04-25 11:13:50 +02:00
. WithHandler ( CommandHandler . Create < string [], DirectoryInfo , int , int , bool , bool >(( databases , serverdatafolder , serverversion , localversion , nobackups , includeuntrackeddatabases ) =>
2025-04-24 16:36:14 +02:00
{
2025-06-11 08:57:14 +02:00
databases = Helper . FindAllDatabases ( databases , serverdatafolder . FullName , includeuntrackeddatabases ). Await ();
2025-04-24 16:36:14 +02:00
if ( databases . Length == 0 )
{
Console . WriteLine ( "No databases found to downgrade" );
return ;
}
static string ReadStream ( string path )
{
using var reader = new StreamReader ( typeof ( Downgrade ). Assembly . GetManifestResourceStream ( path ) ?? throw new InvalidOperationException ());
return reader . ReadToEnd ();
}
static string ResourceNameToPath ( string resourcename )
{
var prefixes = new [] {
typeof ( Program ). Namespace + ".Scripts.Local." ,
typeof ( Program ). Namespace + ".Scripts.Server."
};
foreach ( var prefix in prefixes )
if ( resourcename . StartsWith ( prefix ))
return resourcename . Substring ( prefix . Length );
throw new Exception ( $"Unexpected resource name: {resourcename}" );
}
var serverVersions = typeof ( Downgrade ). Assembly . GetManifestResourceNames ()
. Where ( x => x . Contains ( ".Scripts.Server." ))
. Select ( x => new DowngradeScript ( Version : int . Parse ( ResourceNameToPath ( x ). Split ( '.' ). First ()), Filename : ResourceNameToPath ( x ), Content : ReadStream ( x )))
. ToList ();
var localVersions = typeof ( Downgrade ). Assembly . GetManifestResourceNames ()
. Where ( x => x . Contains ( ".Scripts.Local." ))
. Select ( x => new DowngradeScript ( Version : int . Parse ( ResourceNameToPath ( x ). Split ( '.' ). First ()), Filename : ResourceNameToPath ( x ), Content : ReadStream ( x )))
. ToList ();
foreach ( var db in databases )
{
Console . WriteLine ( $"Examining {db} ..." );
if (! File . Exists ( db ))
{
Console . WriteLine ( $"Database {db} does not exist" );
continue ;
}
2025-04-25 12:30:03 +02:00
int version ;
bool isserverdb ;
2025-04-24 16:36:14 +02:00
try
{
2025-06-11 08:57:14 +02:00
( version , isserverdb ) = Helper . ExamineDatabase ( db ). Await ();
2025-04-24 16:36:14 +02:00
}
catch ( Exception ex )
{
Console . WriteLine ( $"Error reading database {db}: {ex.Message}" );
continue ;
}
2025-04-25 12:30:03 +02:00
Console . WriteLine ( $"Database {db} is version {version} and is a {(isserverdb ? " server " : " local ")} database" );
ApplyDowngrade ( db , version ,
isserverdb ? serverversion : localversion ,
isserverdb ? serverVersions : localVersions ,
2025-06-11 08:57:14 +02:00
nobackups )
. Await ();
2025-04-24 16:36:14 +02:00
}
}));
/// <summary>
/// The downgrade script
/// </summary>
/// <param name="Filename">The path to the assembly resource</param>
/// <param name="Version">The script version</param>
/// <param name="Content">The script contents</param>
private sealed record DowngradeScript ( string Filename , int Version , string Content );
/// <summary>
/// Applies the downgrade scripts to the database
/// </summary>
/// <param name="db">The database path</param>
/// <param name="dbversion">The current database version</param>
/// <param name="targetversion">The target database version</param>
/// <param name="scripts">The downgrade scripts</param>
/// <param name="nobackups">Flag to disable backups</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that completes when the downgrade is done.</returns>
2025-06-11 08:57:14 +02:00
private static async Task ApplyDowngrade ( string db , int dbversion , int targetversion , IEnumerable < DowngradeScript > scripts , bool nobackups )
2025-04-24 16:36:14 +02:00
{
if ( dbversion > targetversion )
{
var requiredVersions = Enumerable . Range ( targetversion + 1 , dbversion - targetversion )
. Reverse ()
. ToList ();
var missingScripts = requiredVersions
. Where ( x => ! scripts . Any ( y => y . Version == x ))
. ToList ();
if ( missingScripts . Count > 0 )
{
Console . WriteLine ( $"Missing downgrade scripts for versions: {string.Join(" , ", missingScripts)}" );
return ;
}
var downgradeScripts = requiredVersions
. Select ( x => scripts . First ( y => y . Version == x ))
. ToList ();
Console . WriteLine ( $"Downgrading {db} from version {dbversion} to {targetversion}" );
if (! nobackups )
Helper . CreateFileBackup ( db );
2025-06-19 11:41:39 +02:00
await using var con = await SQLiteLoader . LoadConnectionAsync ( db )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-18 11:53:11 +02:00
await using var tr = con . BeginTransaction ();
await using var cmd = con . CreateCommand ( tr );
2025-04-24 16:36:14 +02:00
foreach ( var script in downgradeScripts )
{
Console . WriteLine ( $"Applying downgrade script {script.Filename} ..." );
try
{
cmd . SetCommandAndParameters ( script . Content );
2025-06-12 08:34:29 +02:00
var r = await cmd . ExecuteScalarAsync (). ConfigureAwait ( false );
2025-04-24 16:36:14 +02:00
if ( r != null )
throw new UserInformationException ( $"{r}" , "DowngradeScriptFailure" );
2025-06-11 08:57:14 +02:00
await cmd . SetCommandAndParameters ( @"
2025-06-18 10:42:08 +02:00
UPDATE ""Version""
SET ""Version"" = @Version
2025-06-11 08:57:14 +02:00
" )
. SetParameterValue ( "@Version" , script . Version - 1 )
2025-06-12 08:34:29 +02:00
. ExecuteNonQueryAsync ()
. ConfigureAwait ( false );
2025-04-24 16:36:14 +02:00
}
catch ( Exception ex )
{
throw new UserInformationException ( $"Failed applying downgrade script {script.Filename}: {ex.Message}" , "DowngradeScriptFailure" , ex );
}
}
2025-06-12 08:34:29 +02:00
await tr . CommitAsync (). ConfigureAwait ( false );
2025-04-24 16:36:14 +02:00
Console . WriteLine ( $"Downgraded {db} to version {targetversion}" );
}
else if ( dbversion == targetversion )
{
Console . WriteLine ( $"Database {db} is already at version {targetversion}" );
}
else
{
Console . WriteLine ( $"Database {db} is already at version {dbversion} (cannot downgrade to {targetversion})" );
}
}
}