2025-03-13 23:06:32 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-20 15:54:23 +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-03-13 23:06:32 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-20 15:54:23 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-03-13 23:06:32 +01:00
// all copies or substantial portions of the Software.
2025-05-20 15:54:23 +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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2025-04-03 15:46:20 +02:00
#nullable enable
2016-12-29 22:41:13 +01:00
using System ;
using System.Collections.Generic ;
2025-06-19 07:00:14 +02:00
using System.Runtime.CompilerServices ;
using System.Threading ;
2025-05-20 15:54:23 +02:00
using System.Threading.Tasks ;
2019-09-29 20:16:28 -07:00
using Duplicati.Library.Utility ;
2025-05-20 15:59:07 +02:00
using Microsoft.Data.Sqlite ;
2016-12-29 22:41:13 +01:00
namespace Duplicati.Library.Main.Database
{
2025-06-17 21:30:11 +02:00
/// <summary>
/// A local database for purging files from a fileset.
/// </summary>
2016-12-29 22:41:13 +01:00
internal class LocalPurgeDatabase : LocalDeleteDatabase
{
2025-06-17 21:30:11 +02:00
/// <summary>
/// Creates a new instance of the <see cref="LocalPurgeDatabase"/> class.
/// </summary>
/// <param name="path">The path to the database file.</param>
/// <param name="dbnew">The optional existing database instance to use. Used to mimic constructor chaining.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalPurgeDatabase"/>.</returns>
2025-06-19 11:41:39 +02:00
public static async Task < LocalPurgeDatabase > CreateAsync ( string path , LocalPurgeDatabase ? dbnew , CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalPurgeDatabase ();
2025-05-20 15:54:23 +02:00
2025-06-12 08:34:29 +02:00
dbnew = ( LocalPurgeDatabase )
2025-06-19 11:41:39 +02:00
await LocalDeleteDatabase . CreateAsync ( path , "Purge" , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-20 15:54:23 +02:00
2025-05-21 07:24:36 +02:00
dbnew . ShouldCloseConnection = true ;
return dbnew ;
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Creates a new instance of the <see cref="LocalPurgeDatabase"/> class.
/// </summary>
/// <param name="dbparent">The parent database to use.</param>
/// <param name="dbnew">An optional existing database instance to use. Used to mimic constructor chaining.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalPurgeDatabase"/>.</returns>
2025-06-19 07:00:14 +02:00
public static async Task < LocalPurgeDatabase > CreateAsync ( LocalDatabase dbparent , LocalPurgeDatabase ? dbnew , CancellationToken token )
2017-01-05 16:46:01 +01:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalPurgeDatabase ();
2025-05-20 15:54:23 +02:00
2025-06-12 08:34:29 +02:00
dbnew = ( LocalPurgeDatabase )
2025-06-19 07:00:14 +02:00
await LocalDeleteDatabase . CreateAsync ( dbparent , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-20 15:54:23 +02:00
return dbnew ;
2017-01-05 16:46:01 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Creates a new instance of the <see cref="ITemporaryFileset"/> class.
/// </summary>
/// <param name="parentid">The ID of the parent fileset.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="ITemporaryFileset"/>.</returns>
2025-06-19 07:00:14 +02:00
public async Task < ITemporaryFileset > CreateTemporaryFileset ( long parentid , CancellationToken token )
2025-05-22 06:00:01 +02:00
{
2025-06-19 07:00:14 +02:00
return await TemporaryFileset . CreateAsync ( parentid , this , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-22 06:00:01 +02:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Gets the remote volume name for a fileset with the specified ID.
/// </summary>
/// <param name="id">The ID of the fileset.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains the name of the remote volume.</returns>
2025-06-19 07:00:14 +02:00
public async Task < string > GetRemoteVolumeNameForFileset ( long id , CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( @"
2025-05-20 16:27:33 +02:00
SELECT
""B"".""Name""
FROM
2025-06-18 10:42:08 +02:00
""Fileset"" ""A"",
""RemoteVolume"" ""B""
2025-05-20 16:27:33 +02:00
WHERE
""A"".""VolumeID"" = ""B"".""ID""
AND ""A"".""ID"" = @FilesetId
" )
. SetParameterValue ( "@FilesetId" , id );
2025-06-19 07:00:14 +02:00
await using var rd = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
if (! await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
throw new Exception ( $"No remote volume found for fileset with id {id}" );
else
return rd . ConvertValueToString ( 0 ) ?? throw new Exception ( $"Remote volume name for fileset with id {id} is null" );
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Counts the number of orphan files in the database.
/// </summary>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains the count of orphan files.</returns>
2025-06-19 07:00:14 +02:00
internal async Task < long > CountOrphanFiles ( CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-05-20 16:27:33 +02:00
cmd . SetCommandAndParameters ( @"
SELECT COUNT(*)
FROM ""FileLookup""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""FileID""
FROM ""FilesetEntry""
)
" );
2025-06-19 07:00:14 +02:00
await using var rd = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
if ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-05-20 16:27:33 +02:00
return rd . ConvertValueToInt64 ( 0 , 0 );
else
return 0 ;
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Interface for a temporary fileset used for purging files from a fileset.
/// </summary>
2025-06-17 21:04:41 +02:00
public interface ITemporaryFileset : IDisposable , IAsyncDisposable
2016-12-29 22:41:13 +01:00
{
2025-06-17 21:30:11 +02:00
/// <summary>
/// Gets the ID of the parent fileset.
/// </summary>
2016-12-29 22:41:13 +01:00
long ParentID { get ; }
2025-06-17 21:30:11 +02:00
/// <summary>
/// Gets the count of removed files.
/// </summary>
2016-12-29 22:41:13 +01:00
long RemovedFileCount { get ; }
2025-06-17 21:30:11 +02:00
/// <summary>
/// Gets the total size of removed files.
/// </summary>
2016-12-29 22:41:13 +01:00
long RemovedFileSize { get ; }
2025-06-17 21:30:11 +02:00
/// <summary>
/// Gets the count of updated files.
/// </summary>
2025-05-09 11:12:29 +02:00
long UpdatedFileCount { get ; }
2016-12-29 22:41:13 +01:00
2025-06-17 21:30:11 +02:00
/// <summary>
/// Applies a filter to the temporary fileset.
/// </summary>
/// <param name="filter">The filter to apply.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that completes when the filter has been applied.</returns>
2025-06-19 07:00:14 +02:00
Task ApplyFilter ( Library . Utility . IFilter filter , CancellationToken token );
2025-06-17 21:30:11 +02:00
/// <summary>
/// Applies a filter using a custom command.
/// </summary>
/// <param name="filtercommand">The command to execute for filtering.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that completes when the filter has been applied.</returns>
2025-06-19 07:00:14 +02:00
Task ApplyFilter ( Func < SqliteCommand , long , string , Task < int >> filtercommand , CancellationToken token );
2025-06-17 21:30:11 +02:00
/// <summary>
/// Converts the temporary fileset to a permanent fileset.
/// </summary>
/// <param name="name">The name of the new fileset.</param>
/// <param name="timestamp">The timestamp for the new fileset.</param>
/// <param name="isFullBackup">Indicates if this is a full backup.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains a tuple with the remote volume ID and the new fileset ID.</returns>
2025-06-19 07:00:14 +02:00
Task < Tuple < long , long >> ConvertToPermanentFileset ( string name , DateTime timestamp , bool isFullBackup , CancellationToken token );
2025-06-17 21:30:11 +02:00
/// <summary>
/// Lists all deleted files in the temporary fileset.
/// </summary>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>An asynchronous enumerable of key-value pairs where the key is the file path and the value is the file size.</returns>
2025-06-19 07:00:14 +02:00
IAsyncEnumerable < KeyValuePair < string , long >> ListAllDeletedFiles ( CancellationToken token );
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// A temporary fileset implementation for purging files from a fileset.
/// </summary>
2016-12-29 22:41:13 +01:00
private class TemporaryFileset : ITemporaryFileset
{
2025-06-17 21:30:11 +02:00
/// <summary>
/// The name of the temporary table used for storing deleted files.
/// </summary>
2025-05-20 15:54:23 +02:00
private string m_tablename = null !;
2025-06-17 21:30:11 +02:00
/// <summary>
/// The database instance used for queries.
/// </summary>
2025-05-20 15:54:23 +02:00
private LocalDatabase m_db = null !;
2016-12-29 22:41:13 +01:00
public long ParentID { get ; private set ; }
public long RemovedFileCount { get ; private set ; }
public long RemovedFileSize { get ; private set ; }
2025-05-09 11:12:29 +02:00
public long UpdatedFileCount { get ; private set ; }
2016-12-29 22:41:13 +01:00
2025-06-17 21:30:11 +02:00
/// <summary>
/// Calling this constructor will throw an exception. Use CreateAsync instead.
/// </summary>
2025-05-20 15:54:23 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateAsync instead.")]
2025-05-20 15:59:07 +02:00
public TemporaryFileset ( long parentid , LocalPurgeDatabase parentdb , SqliteConnection connection , SqliteTransaction transaction )
2016-12-29 22:41:13 +01:00
{
2025-05-20 15:54:23 +02:00
throw new NotImplementedException ( "Use CreateAsync instead" );
}
2016-12-29 22:41:13 +01:00
2025-06-17 21:30:11 +02:00
/// <summary>
/// Private constructor to prevent direct instantiation.
/// </summary>
2025-05-20 15:54:23 +02:00
private TemporaryFileset () { }
2025-06-17 21:30:11 +02:00
/// <summary>
/// Creates a new instance of the <see cref="TemporaryFileset"/> class asynchronously.
/// </summary>
/// <param name="parentid">The ID of the parent fileset.</param>
/// <param name="db">The database instance to use.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="TemporaryFileset"/>.</returns>
2025-06-19 07:00:14 +02:00
public static async Task < TemporaryFileset > CreateAsync ( long parentid , LocalDatabase db , CancellationToken token )
2025-05-20 15:54:23 +02:00
{
var tempf = new TemporaryFileset ()
{
ParentID = parentid ,
m_db = db ,
2025-09-05 14:06:55 +02:00
m_tablename = $"TempDeletedFilesTable-{Library.Utility.Utility.GetHexGuid()}"
2025-05-20 15:54:23 +02:00
};
2025-06-18 11:53:11 +02:00
await using ( var cmd = db . Connection . CreateCommand ())
2025-05-20 16:27:33 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{tempf.m_tablename}"" (
""FileID"" INTEGER PRIMARY KEY
)
2025-06-19 07:00:14 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-20 15:54:23 +02:00
return tempf ;
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Applies a filter to the temporary fileset using a custom command.
/// </summary>
/// <param name="filtercommand">The command to execute for filtering.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that completes when the filter has been applied.</returns>
2025-06-19 07:00:14 +02:00
public async Task ApplyFilter ( Func < SqliteCommand , long , string , Task < int >> filtercommand , CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-05-09 11:12:29 +02:00
int updated ;
2025-06-18 11:53:11 +02:00
await using ( var cmd = m_db . Connection . CreateCommand ())
2025-06-12 08:34:29 +02:00
updated = await filtercommand ( cmd , ParentID , m_tablename )
. ConfigureAwait ( false );
2025-03-14 12:13:17 +01:00
2025-06-19 07:00:14 +02:00
await PostFilterChecks ( updated , token ). ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Applies a filter to the temporary fileset.
/// </summary>
/// <param name="filter">The filter to apply.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that completes when the filter has been applied.</returns>
2025-06-19 07:00:14 +02:00
public async Task ApplyFilter ( Library . Utility . IFilter filter , CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2019-09-29 20:16:28 -07:00
if ( Library . Utility . Utility . IsFSCaseSensitive && filter is FilterExpression expression && expression . Type == Duplicati . Library . Utility . FilterType . Simple )
2016-12-29 22:41:13 +01:00
{
// File list based
2019-09-29 09:51:14 -07:00
// unfortunately we cannot do this if the filesystem is not case-sensitive as
2016-12-29 22:41:13 +01:00
// SQLite only supports ASCII compares
2019-09-29 20:16:28 -07:00
var p = expression . GetSimpleList ();
2025-09-05 14:06:55 +02:00
var filenamestable = $"Filenames-{Library.Utility.Utility.GetHexGuid()}" ;
2025-06-18 11:53:11 +02:00
await using var cmd = m_db . Connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
2025-05-20 16:27:33 +02:00
CREATE TEMPORARY TABLE ""{filenamestable}"" (
""Path"" TEXT NOT NULL
)
2025-06-19 07:00:14 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
await cmd . SetCommandAndParameters ( $@"
2025-05-20 16:27:33 +02:00
INSERT INTO ""{filenamestable}"" (""Path"")
VALUES (@Path)
" )
2025-06-19 07:00:14 +02:00
. PrepareAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
foreach ( var s in p )
await cmd . SetParameterValue ( "@Path" , s )
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
2025-06-18 11:18:25 +02:00
await cmd . SetCommandAndParameters ( $@"
2025-05-20 16:27:33 +02:00
INSERT INTO ""{m_tablename}"" (""FileID"")
SELECT DISTINCT ""A"".""FileID""
FROM
2025-06-18 10:42:08 +02:00
""FilesetEntry"" ""A"",
""File"" ""B""
2025-05-20 16:27:33 +02:00
WHERE
""A"".""FilesetID"" = @FilesetId
AND ""A"".""FileID"" = ""B"".""ID""
AND ""B"".""Path"" IN ""{filenamestable}""
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , ParentID )
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
await cmd
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{filenamestable}"" " , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
else
{
// Do row-wise iteration
2025-03-14 14:34:56 +01:00
var values = new object [ 2 ];
2025-06-18 11:53:11 +02:00
await using var cmd = m_db . Connection . CreateCommand ();
await using var cmd2 = m_db . Connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
await cmd2 . SetCommandAndParameters ( $@"
2025-05-20 16:27:33 +02:00
INSERT INTO ""{m_tablename}"" (""FileID"")
VALUES (@FileId)
" )
2025-06-19 07:00:14 +02:00
. PrepareAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
cmd . SetCommandAndParameters ( @"
2025-05-20 16:27:33 +02:00
SELECT
""B"".""Path"",
""A"".""FileID""
FROM
2025-06-18 10:42:08 +02:00
""FilesetEntry"" ""A"",
""File"" ""B""
2025-05-20 16:27:33 +02:00
WHERE
""A"".""FilesetID"" = @FilesetId
AND ""A"".""FileID"" = ""B"".""ID""
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , ParentID );
2025-06-19 07:00:14 +02:00
await using var rd = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
while ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
rd . GetValues ( values );
var path = values [ 0 ] as string ;
if ( path != null && Library . Utility . FilterExpression . Matches ( filter , path . ToString ()))
{
await cmd2
. SetParameterValue ( "@FileId" , values [ 1 ])
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
}
2016-12-29 22:41:13 +01:00
}
}
2025-06-19 07:00:14 +02:00
await PostFilterChecks ( 0 , token ). ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
2025-06-17 21:30:11 +02:00
/// <summary>
/// Applies the filter checks after filtering.
/// </summary>
/// <param name="updated">The number of files updated by the filter.</param>
2025-06-19 07:00:14 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 21:30:11 +02:00
/// <returns>A task that completes when the checks have been applied.</returns>
2025-06-19 07:00:14 +02:00
private async Task PostFilterChecks ( int updated , CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_db . Connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
UpdatedFileCount = updated ;
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
RemovedFileCount = await cmd . ExecuteScalarInt64Async ( $@"
2025-05-20 16:27:33 +02:00
SELECT COUNT(*)
FROM ""{m_tablename}""
2025-06-19 07:00:14 +02:00
" , 0 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
RemovedFileSize = await cmd . ExecuteScalarInt64Async ( $@"
2025-05-20 16:27:33 +02:00
SELECT SUM(""C"".""Length"")
2025-05-09 11:12:29 +02:00
FROM
2025-06-18 10:42:08 +02:00
""{m_tablename}"" ""A"",
""FileLookup"" ""B"",
""Blockset"" ""C"",
""Metadataset"" ""D""
2025-05-09 11:12:29 +02:00
WHERE
""A"".""FileID"" = ""B"".""ID""
2025-05-20 16:27:33 +02:00
AND (
""B"".""BlocksetID"" = ""C"".""ID""
OR (
""B"".""MetadataID"" = ""D"".""ID""
AND ""D"".""BlocksetID"" = ""C"".""ID""
)
)
2025-06-19 07:00:14 +02:00
" , 0 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
var filesetcount = await cmd . SetCommandAndParameters ( $@"
2025-05-20 16:27:33 +02:00
SELECT COUNT(*)
FROM ""FilesetEntry""
2025-06-18 10:42:08 +02:00
WHERE ""FilesetID"" = @ParentID
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@ParentID" , ParentID )
2025-06-19 07:00:14 +02:00
. ExecuteScalarInt64Async ( 0 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 16:27:33 +02:00
2025-06-18 11:18:25 +02:00
if ( filesetcount == RemovedFileCount )
throw new Interface . UserInformationException ( $"Refusing to purge {RemovedFileCount} files from fileset with ID {ParentID}, as that would remove the entire fileset.\nTo delete a fileset, use the \" delete \ " command." , "PurgeWouldRemoveEntireFileset" );
2016-12-29 22:41:13 +01:00
}
2025-06-19 07:00:14 +02:00
/// <inheritdoc/>
public async Task < Tuple < long , long >> ConvertToPermanentFileset ( string name , DateTime timestamp , bool isFullBackup , CancellationToken token )
2019-09-01 13:13:45 -04:00
{
2025-06-12 08:34:29 +02:00
var remotevolid =
2025-06-19 07:00:14 +02:00
await m_db
. RegisterRemoteVolume ( name , RemoteVolumeType . Files , RemoteVolumeState . Temporary , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-19 07:00:14 +02:00
var filesetid = await m_db
. CreateFileset ( remotevolid , timestamp , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-19 07:00:14 +02:00
await m_db
. UpdateFullBackupStateInFileset ( filesetid , isFullBackup , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-09-10 12:55:29 -04:00
2025-06-18 11:53:11 +02:00
await using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction ))
2025-05-20 16:27:33 +02:00
await cmd . SetCommandAndParameters ( $@"
INSERT INTO ""FilesetEntry"" (
""FilesetID"",
""FileID"",
""Lastmodified""
)
SELECT
@TargetFilesetId,
""FileID"",
""LastModified""
FROM ""FilesetEntry""
WHERE
""FilesetID"" = @SourceFilesetId
AND ""FileID"" NOT IN ""{m_tablename}""
" )
. SetParameterValue ( "@TargetFilesetId" , filesetid )
. SetParameterValue ( "@SourceFilesetId" , ParentID )
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
return new Tuple < long , long >( remotevolid , filesetid );
}
2025-03-14 12:13:17 +01:00
2025-06-19 07:00:14 +02:00
/// <inheritdoc/>
public async IAsyncEnumerable < KeyValuePair < string , long >> ListAllDeletedFiles ([ EnumeratorCancellation ] CancellationToken token )
2016-12-29 22:41:13 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_db . Connection . CreateCommand ();
await using var rd = await cmd . ExecuteReaderAsync ( $@"
2025-05-20 16:27:33 +02:00
SELECT
""B"".""Path"",
""C"".""Length""
FROM
2025-06-18 10:42:08 +02:00
""{m_tablename}"" ""A"",
""File"" ""B"",
""Blockset"" ""C""
2025-05-20 16:27:33 +02:00
WHERE
""A"".""FileID"" = ""B"".""ID""
AND ""B"".""BlocksetID"" = ""C"".""ID""
2025-06-19 07:00:14 +02:00
" , token )
. ConfigureAwait ( false );
while ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
yield return new KeyValuePair < string , long >(
rd . ConvertValueToString ( 0 ) ?? "" ,
rd . ConvertValueToInt64 ( 1 )
);
2016-12-29 22:41:13 +01:00
}
public void Dispose ()
2025-05-20 16:15:37 +02:00
{
2025-06-17 21:04:41 +02:00
DisposeAsync (). AsTask (). Await ();
2025-05-20 16:15:37 +02:00
}
2025-06-17 21:04:41 +02:00
public async ValueTask DisposeAsync ()
2016-12-29 22:41:13 +01:00
{
try
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_db . Connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
await cmd
2025-06-19 07:00:14 +02:00
. ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tablename}""" , default )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2016-12-29 22:41:13 +01:00
}
2025-05-20 16:27:33 +02:00
catch { }
2016-12-29 22:41:13 +01:00
}
}
2025-03-14 12:13:17 +01:00
}
2016-12-29 22:41:13 +01:00
}