2025-03-10 22:01:14 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-02-28 15:45:30 +01:00
// https://duplicati.com, hello@duplicati.com
2025-05-19 20:38:34 +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
2024-02-28 15:45:30 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-19 20:38:34 +02:00
//
// The above copyright notice and this permission notice shall be included in
2024-02-28 15:45:30 +01:00
// all copies or substantial portions of the Software.
2025-05-19 20:38:34 +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.
2013-03-27 16:06:45 +01:00
2025-04-03 15:24:57 +02:00
#nullable enable
2013-03-27 16:06:45 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2025-06-18 16:07:46 +02:00
using System.Runtime.CompilerServices ;
using System.Threading ;
2025-05-19 20:43:45 +02:00
using System.Threading.Tasks ;
2019-12-09 20:31:15 -08:00
using Duplicati.Library.Interface ;
2025-05-19 20:43:45 +02:00
using Microsoft.Data.Sqlite ;
2013-03-27 16:06:45 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Database
2013-03-27 16:06:45 +01:00
{
2025-06-17 18:01:12 +02:00
/// <summary>
/// A local database for deleting filesets and blocks.
/// </summary>
2016-09-15 11:39:27 +02:00
internal class LocalDeleteDatabase : LocalDatabase
{
2018-03-12 14:07:11 +01:00
/// <summary>
2025-06-17 18:01:12 +02:00
/// The tag used for logging.
2018-03-12 14:07:11 +01:00
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < LocalDeleteDatabase >();
2024-11-01 14:50:26 +01:00
/// <summary>
2025-06-17 18:01:12 +02:00
/// Flag for toggling temporary tables; set to empty string if debugging.
2024-11-01 14:50:26 +01:00
/// </summary>
private const string TEMPORARY = "TEMPORARY" ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// SQL command to register a duplicate block.
/// </summary>
2025-06-18 10:42:08 +02:00
private const string REGISTER_COMMAND = @"
INSERT OR IGNORE INTO ""DuplicateBlock"" (
""BlockID"",
""VolumeID""
)
SELECT
""ID"",
@VolumeId
FROM ""Block""
WHERE
""Hash"" = @Hash
AND ""Size"" = @Size
" ;
2025-04-03 15:24:57 +02:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// The command to register a duplicate block.
/// </summary>
2025-05-19 20:43:45 +02:00
private SqliteCommand m_registerDuplicateBlockCommand = null !;
2013-03-27 16:06:45 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Creates a new instance of the <see cref="LocalDeleteDatabase"/> class.
/// </summary>
/// <param name="path">The path to the database file.</param>
/// <param name="operation">The operation name.</param>
/// <param name="dbnew">An optional existing database instance to use. Used to mimic constructor chaining.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token"> A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalDeleteDatabase"/>.</returns>
2025-06-19 11:41:39 +02:00
public static async Task < LocalDeleteDatabase > CreateAsync ( string path , string operation , LocalDeleteDatabase ? dbnew , CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalDeleteDatabase ();
2025-05-19 20:43:45 +02:00
2025-06-12 08:34:29 +02:00
dbnew = ( LocalDeleteDatabase )
2025-06-19 11:41:39 +02:00
await CreateLocalDatabaseAsync ( path , operation , true , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 20:43:45 +02:00
2025-06-12 08:34:29 +02:00
dbnew . m_registerDuplicateBlockCommand =
2025-06-18 16:07:46 +02:00
await dbnew . Connection . CreateCommandAsync ( REGISTER_COMMAND , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-21 07:24:36 +02:00
return dbnew ;
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Creates a new instance of the <see cref="LocalDeleteDatabase"/> class with a parent database.
/// </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-18 16:07:46 +02:00
/// <param name="token"> A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalDeleteDatabase"/>.</returns>
2025-06-18 16:07:46 +02:00
public static async Task < LocalDeleteDatabase > CreateAsync ( LocalDatabase dbparent , LocalDeleteDatabase ? dbnew , CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalDeleteDatabase ();
2025-05-19 20:43:45 +02:00
2025-06-12 08:34:29 +02:00
dbnew = ( LocalDeleteDatabase )
2025-06-18 16:07:46 +02:00
await CreateLocalDatabaseAsync ( dbparent , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 20:43:45 +02:00
2025-06-12 08:34:29 +02:00
dbnew . m_registerDuplicateBlockCommand =
2025-06-18 16:07:46 +02:00
await dbnew . Connection . CreateCommandAsync ( REGISTER_COMMAND , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 20:43:45 +02:00
return dbnew ;
2016-09-15 11:39:27 +02:00
}
2018-10-14 20:10:32 -07:00
2016-09-15 11:39:27 +02:00
/// <summary>
/// Drops all entries related to operations listed in the table.
/// </summary>
2025-06-17 18:01:12 +02:00
/// <param name="toDelete">The fileset entries to delete.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token"> A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>An async enumerable of key-value pairs, where the key is the fileset name and the value is the size of the fileset.</returns>
2025-06-18 16:07:46 +02:00
public async IAsyncEnumerable < KeyValuePair < string , long >> DropFilesetsFromTable ( DateTime [] toDelete , [ EnumeratorCancellation ] CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var deleted = 0 ;
2019-07-31 07:09:37 -07:00
2025-06-18 16:07:46 +02:00
await using ( var tempTable = await TemporaryDbValueList . CreateAsync ( this , toDelete . Select ( Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ), token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
deleted += await (
await cmd . SetCommandAndParameters ( @"
2025-06-12 08:34:29 +02:00
DELETE FROM ""Fileset""
WHERE ""Timestamp"" IN (@Timestamps)
" )
2025-06-18 16:07:46 +02:00
. ExpandInClauseParameterMssqliteAsync ( "@Timestamps" , tempTable , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false )
)
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-07-31 07:09:37 -07:00
2025-06-18 11:18:25 +02:00
if ( deleted != toDelete . Length )
throw new Exception ( $"Unexpected number of deleted filesets {deleted} vs {toDelete.Length}" );
2024-11-01 16:45:25 +01:00
2025-06-18 11:18:25 +02:00
//Then we delete anything that is no longer being referenced
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""FilesetEntry""
WHERE ""FilesetID"" NOT IN (
SELECT DISTINCT ""ID""
FROM ""Fileset""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""ChangeJournalData""
WHERE ""FilesetID"" NOT IN (
SELECT DISTINCT ""ID""
FROM ""Fileset""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""FileLookup""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""FileID""
FROM ""FilesetEntry""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""Metadataset""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""MetadataID""
FROM ""FileLookup""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""Blockset""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""BlocksetID""
FROM ""FileLookup""
UNION
SELECT DISTINCT ""BlocksetID""
FROM ""Metadataset""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""BlocksetEntry""
WHERE ""BlocksetID"" NOT IN (
SELECT DISTINCT ""ID""
FROM ""Blockset""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""BlocklistHash""
WHERE ""BlocksetID"" NOT IN (
SELECT DISTINCT ""ID""
FROM ""Blockset""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2024-11-01 16:45:25 +01:00
2025-06-18 11:18:25 +02:00
//We save the block info for the remote files, before we delete it
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
INSERT INTO ""DeletedBlock"" (
""Hash"",
""Size"",
""VolumeID""
)
SELECT
""Hash"",
""Size"",
""VolumeID""
FROM ""Block""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""BlockID"" AS ""BlockID""
FROM ""BlocksetEntry""
UNION
SELECT DISTINCT ""ID""
FROM
""Block"",
""BlocklistHash""
WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
await cmd . ExecuteNonQueryAsync ( @"
2025-05-19 21:51:28 +02:00
DELETE FROM ""Block""
WHERE ""ID"" NOT IN (
SELECT DISTINCT ""BlockID""
FROM ""BlocksetEntry""
UNION
SELECT DISTINCT ""ID""
FROM
""Block"",
""BlocklistHash""
WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash""
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2020-02-23 13:23:21 -06:00
2025-06-18 11:18:25 +02:00
//Find all remote filesets that are no longer required, and mark them as deleting
var updated = await cmd . SetCommandAndParameters ( @"
2025-05-19 21:51:28 +02:00
UPDATE ""RemoteVolume""
SET ""State"" = @NewState
WHERE
""Type"" = @CurrentType
AND ""State"" IN (@AllowedStates)
AND ""ID"" NOT IN (
SELECT ""VolumeID""
FROM ""Fileset""
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@NewState" , RemoteVolumeState . Deleting . ToString ())
. SetParameterValue ( "@CurrentType" , RemoteVolumeType . Files . ToString ())
. ExpandInClauseParameterMssqlite ( "@AllowedStates" , [
RemoteVolumeState . Uploaded . ToString (),
2025-05-19 21:51:28 +02:00
RemoteVolumeState . Verified . ToString (),
RemoteVolumeState . Temporary . ToString (),
RemoteVolumeState . Deleting . ToString ()
2025-06-18 11:18:25 +02:00
])
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2020-02-23 13:23:21 -06:00
2025-06-18 11:18:25 +02:00
if ( deleted != updated )
throw new Exception ( $"Unexpected number of remote volumes marked as deleted. Found {deleted} filesets, but {updated} volumes" );
2020-11-21 13:58:16 -08:00
2025-06-18 11:18:25 +02:00
cmd . SetCommandAndParameters ( @"
2025-05-19 21:51:28 +02:00
SELECT
""Name"",
""Size""
FROM ""RemoteVolume""
WHERE
""Type"" = @Type
AND ""State"" = @State
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ())
. SetParameterValue ( "@State" , RemoteVolumeState . Deleting . ToString ());
2025-06-18 16:07:46 +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
yield return new KeyValuePair < string , long >(
rd . ConvertValueToString ( 0 ) ?? "" ,
rd . ConvertValueToInt64 ( 1 )
);
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
2019-12-12 18:38:17 -08:00
/// <summary>
/// Returns a collection of IListResultFilesets, where the Version is the backup version number
2024-07-26 00:08:45 -04:00
/// exposed to the user. This is in contrast to other cases where the Version is the ID in the
2019-12-12 18:38:17 -08:00
/// Fileset table.
/// </summary>
2025-06-18 16:07:46 +02:00
/// <param name="token"> A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>An async enumerable of IListResultFileset.</returns>
2025-06-18 16:07:46 +02:00
internal async IAsyncEnumerable < IListResultFileset > FilesetsWithBackupVersion ([ EnumeratorCancellation ] CancellationToken token )
2019-12-09 20:31:15 -08:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
// TODO check if this is still the case? (shouldn't be with new sqlite driver):
// We can also use the ROW_NUMBER() window function to generate the backup versions,
// but this requires at least SQLite 3.25, which is not available in some common
// distributions (e.g., Debian) currently.
cmd . SetCommandAndParameters ( @"
2025-05-19 21:03:40 +02:00
SELECT
""IsFullBackup"",
""Timestamp""
FROM ""Fileset""
ORDER BY ""Timestamp"" DESC
" );
2025-06-18 16:07:46 +02:00
await using var reader = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
2025-06-18 11:18:25 +02:00
int version = 0 ;
2025-06-18 16:07:46 +02:00
while ( await reader . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
yield return new ListResultFileset (
version ++,
reader . GetInt32 ( 0 ),
ParseFromEpochSeconds ( reader . ConvertValueToInt64 ( 1 )). ToLocalTime (),
- 1L ,
- 1L
);
2019-12-09 20:31:15 -08:00
}
}
2025-06-17 18:01:12 +02:00
/// <summary>
/// Represents the usage of a volume in the database.
/// </summary>
2016-09-15 11:39:27 +02:00
private struct VolumeUsage
{
2025-06-17 18:01:12 +02:00
/// <summary>
/// The name of the volume.
/// </summary>
2019-10-19 10:56:21 -07:00
public readonly string Name ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The size of the data stored in the volume.
/// </summary>
2019-10-19 10:56:21 -07:00
public readonly long DataSize ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The size of the data that is no longer needed in the volume.
/// </summary>
2019-10-19 10:56:21 -07:00
public readonly long WastedSize ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The size of the data that is compressed in the volume.
/// </summary>
2019-10-19 10:56:21 -07:00
public readonly long CompressedSize ;
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="VolumeUsage"/> struct.
/// </summary>
/// <param name="name">The name of the volume.</param>
/// <param name="datasize">The size of the data stored in the volume.</param>
/// <param name="wastedsize">The size of the data that is no longer needed in the volume.</param>
/// <param name="compressedsize">The size of the data that is compressed in the volume.</param>
2016-09-15 11:39:27 +02:00
public VolumeUsage ( string name , long datasize , long wastedsize , long compressedsize )
{
2025-03-14 14:34:56 +01:00
Name = name ;
DataSize = datasize ;
WastedSize = wastedsize ;
CompressedSize = compressedsize ;
2016-09-15 11:39:27 +02:00
}
}
2013-03-27 16:06:45 +01:00
2016-09-15 11:39:27 +02:00
/// <summary>
/// Returns the number of bytes stored in each volume,
/// and the number of bytes no longer needed in each volume.
/// The sizes are the uncompressed values.
/// </summary>
2025-06-18 16:07:46 +02:00
/// <param name="token"> A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>
/// An asynchronous enumerable of <see cref="VolumeUsage"/>, holding the name of the volume,
/// the size of the data stored in the volume, the size of the data that is no longer needed in the volume,
/// and the size of the data that is compressed in the volume.
/// </returns>
2025-06-18 16:07:46 +02:00
private async IAsyncEnumerable < VolumeUsage > GetWastedSpaceReport ([ EnumeratorCancellation ] CancellationToken token )
2016-09-15 11:39:27 +02:00
{
var tmptablename = "UsageReport-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2024-11-01 16:45:25 +01:00
2025-05-19 21:51:28 +02:00
var usedBlocks = @"
SELECT
SUM(""Block"".""Size"") AS ""ActiveSize"",
""Block"".""VolumeID"" AS ""VolumeID"" FROM ""Block"",
""Remotevolume""
WHERE
""Block"".""VolumeID"" = ""Remotevolume"".""ID""
AND ""Block"".""ID"" NOT IN (
SELECT ""Block"".""ID""
FROM
""Block"",
""DeletedBlock""
WHERE
""Block"".""Hash"" = ""DeletedBlock"".""Hash""
AND ""Block"".""Size"" = ""DeletedBlock"".""Size""
AND ""Block"".""VolumeID"" = ""DeletedBlock"".""VolumeID""
)
GROUP BY ""Block"".""VolumeID""
" ;
var lastmodifiedFile = @"
SELECT
""Block"".""VolumeID"" AS ""VolumeID"",
""Fileset"".""Timestamp"" AS ""Sorttime""
FROM
""Fileset"",
""FilesetEntry"",
""FileLookup"",
""BlocksetEntry"",
""Block""
WHERE
""FilesetEntry"".""FileID"" = ""FileLookup"".""ID""
AND ""FileLookup"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID""
" ;
var lastmodifiedMetadata = @"
SELECT
""Block"".""VolumeID"" AS ""VolumeID"",
""Fileset"".""Timestamp"" AS ""Sorttime""
FROM
""Fileset"",
""FilesetEntry"",
""FileLookup"",
""BlocksetEntry"",
""Block"",
""Metadataset""
WHERE
""FilesetEntry"".""FileID"" = ""FileLookup"".""ID""
AND ""FileLookup"".""MetadataID"" = ""Metadataset"".""ID""
AND ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID""
AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID""
AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID""
" ;
var scantime = @ $"
SELECT
""VolumeID"" AS ""VolumeID"",
MIN(""Sorttime"") AS ""Sorttime""
FROM (
{lastmodifiedFile}
UNION {lastmodifiedMetadata}
)
GROUP BY ""VolumeID""
" ;
var active = @ $"
SELECT
""A"".""ActiveSize"" AS ""ActiveSize"",
0 AS ""InactiveSize"",
""A"".""VolumeID"" AS ""VolumeID"",
CASE
WHEN ""B"".""Sorttime"" IS NULL
THEN 0
ELSE ""B"".""Sorttime""
END AS ""Sorttime""
2025-06-18 10:42:08 +02:00
FROM ({usedBlocks}) ""A""
LEFT OUTER JOIN ({scantime}) ""B""
2025-05-19 21:51:28 +02:00
ON ""B"".""VolumeID"" = ""A"".""VolumeID""
" ;
var inactive = @"
SELECT
0 AS ""ActiveSize"",
SUM(""Size"") AS ""InactiveSize"",
""VolumeID"" AS ""VolumeID"",
0 AS ""SortScantime""
FROM ""DeletedBlock""
GROUP BY ""VolumeID""
" ;
var empty = @"
SELECT
0 AS ""ActiveSize"",
0 AS ""InactiveSize"",
""Remotevolume"".""ID"" AS ""VolumeID"",
0 AS ""SortScantime""
FROM ""Remotevolume""
WHERE
""Remotevolume"".""Type"" = @Type
AND ""Remotevolume"".""State"" IN (@AllowedStates)
AND ""Remotevolume"".""ID"" NOT IN (
SELECT ""VolumeID""
FROM ""Block""
)
" ;
var combined = $"{active} UNION {inactive} UNION {empty}" ;
var collected = @ $"
SELECT
""VolumeID"" AS ""VolumeID"",
SUM(""ActiveSize"") AS ""ActiveSize"",
SUM(""InactiveSize"") AS ""InactiveSize"",
MAX(""Sorttime"") AS ""Sorttime""
FROM ({combined})
GROUP BY ""VolumeID""
" ;
var createtable = $"{@$" CREATE { TEMPORARY } TABLE "" { tmptablename } "" AS "}{collected}" ;
2024-11-01 16:45:25 +01:00
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
try
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:18:25 +02:00
await cmd
. SetCommandAndParameters ( createtable )
. SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ())
. ExpandInClauseParameterMssqlite ( "@AllowedStates" , [
RemoteVolumeState . Uploaded . ToString (),
2025-05-19 21:51:28 +02:00
RemoteVolumeState . Verified . ToString ()
2025-06-18 11:18:25 +02:00
])
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
cmd . SetCommandAndParameters ( $@"
2025-05-19 21:51:28 +02:00
SELECT
""A"".""Name"",
""B"".""ActiveSize"",
""B"".""InactiveSize"",
""A"".""Size""
FROM
2025-06-18 10:42:08 +02:00
""Remotevolume"" ""A"",
""{tmptablename}"" ""B""
2025-05-19 21:51:28 +02:00
WHERE ""A"".""ID"" = ""B"".""VolumeID""
ORDER BY ""B"".""Sorttime"" ASC
" );
2025-06-18 16:07:46 +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
yield return new VolumeUsage (
rd . ConvertValueToString ( 0 ) ?? "" ,
rd . ConvertValueToInt64 ( 1 , 0 ) + rd . ConvertValueToInt64 ( 2 , 0 ),
rd . ConvertValueToInt64 ( 2 , 0 ),
rd . ConvertValueToInt64 ( 3 , 0 )
);
}
finally
{
try
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:18:25 +02:00
await cmd
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{tmptablename}"" " , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2016-09-15 11:39:27 +02:00
}
2025-06-18 11:18:25 +02:00
catch { }
2016-09-15 11:39:27 +02:00
}
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Generates a report on the volumes that can be reclaimed or compacted.
/// </summary>
2016-09-15 11:39:27 +02:00
public interface ICompactReport
{
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the volumes that can be deleted.
/// </summary>
2016-09-15 11:39:27 +02:00
IEnumerable < string > DeleteableVolumes { get ; }
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the volumes that can be compacted.
/// </summary>
2016-09-15 11:39:27 +02:00
IEnumerable < string > CompactableVolumes { get ; }
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the report on whether reclamation or compaction should be performed.
/// </summary>
2016-09-15 11:39:27 +02:00
bool ShouldReclaim { get ; }
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the report on whether compaction should be performed.
/// </summary>
2016-09-15 11:39:27 +02:00
bool ShouldCompact { get ; }
2025-06-17 18:01:12 +02:00
/// <summary>
/// Emits the report data to the log.
/// </summary>
2024-11-01 16:45:25 +01:00
void ReportCompactData ();
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// A report on the volumes that can be reclaimed or compacted.
/// </summary>
2016-09-15 11:39:27 +02:00
private class CompactReport : ICompactReport
{
2025-06-17 18:01:12 +02:00
/// <summary>
/// The report to emit to the log.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly IEnumerable < VolumeUsage > m_report ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The volumes that can be fully deleted.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly IEnumerable < VolumeUsage > m_cleandelete ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The volumes that have wasted space.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly IEnumerable < VolumeUsage > m_wastevolumes ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The volumes that are smaller than the specified size.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly IEnumerable < VolumeUsage > m_smallvolumes ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The count of volumes that can be fully deleted.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_deletablevolumes ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The total size of wasted space across all volumes.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_wastedspace ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The total size of small volumes.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_smallspace ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The total size of all data across all volumes.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_fullsize ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The count of small volumes.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_smallvolumecount ;
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// The threshold for wasted space percentage to trigger compaction.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_wastethreshold ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The size of the volume to trigger compaction.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_volsize ;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The maximum number of small files to trigger compaction.
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly long m_maxsmallfilecount ;
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="CompactReport"/> class.
/// </summary>
/// <param name="volsize">The size of the volume to trigger compaction.</param>
/// <param name="wastethreshold">The threshold for wasted space percentage to trigger compaction.</param>
/// <param name="smallfilesize">The size of small files to trigger compaction.</param>
/// <param name="maxsmallfilecount">The maximum number of small files to trigger compaction.</param>
/// <param name="report">The report data containing volume usage information.</param>
2016-09-15 11:39:27 +02:00
public CompactReport ( long volsize , long wastethreshold , long smallfilesize , long maxsmallfilecount , IEnumerable < VolumeUsage > report )
{
m_report = report ;
2024-11-01 16:45:25 +01:00
2015-09-16 21:25:32 +02:00
m_cleandelete = ( from n in m_report where n . DataSize <= n . WastedSize select n ). ToArray ();
2016-09-15 11:39:27 +02:00
m_wastevolumes = from n in m_report where (((( n . WastedSize / ( float ) n . DataSize ) * 100 ) >= wastethreshold ) || ((( n . WastedSize / ( float ) volsize ) * 100 ) >= wastethreshold )) && ! m_cleandelete . Contains ( n ) select n ;
m_smallvolumes = from n in m_report where n . CompressedSize <= smallfilesize && ! m_cleandelete . Contains ( n ) select n ;
2013-03-27 16:06:45 +01:00
2016-09-15 11:39:27 +02:00
m_wastethreshold = wastethreshold ;
m_volsize = volsize ;
2013-08-24 21:16:44 +02:00
m_maxsmallfilecount = maxsmallfilecount ;
2013-03-27 16:06:45 +01:00
2016-09-15 11:39:27 +02:00
m_deletablevolumes = m_cleandelete . Count ();
m_fullsize = report . Select ( x => x . DataSize ). Sum ();
2024-11-01 16:45:25 +01:00
2016-09-15 11:39:27 +02:00
m_wastedspace = m_wastevolumes . Select ( x => x . WastedSize ). Sum ();
m_smallspace = m_smallvolumes . Select ( x => x . CompressedSize ). Sum ();
2013-08-24 21:16:44 +02:00
m_smallvolumecount = m_smallvolumes . Count ();
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Emits the report data to the log.
/// </summary>
2018-03-12 14:07:11 +01:00
public void ReportCompactData ()
2013-08-22 20:52:54 +02:00
{
2013-08-25 07:50:24 +02:00
var wastepercentage = (( m_wastedspace / ( float ) m_fullsize ) * 100 );
2018-03-12 14:07:11 +01:00
Logging . Log . WriteVerboseMessage ( LOGTAG , "FullyDeletableCount" , "Found {0} fully deletable volume(s)" , m_deletablevolumes );
Logging . Log . WriteVerboseMessage ( LOGTAG , "SmallVolumeCount" , "Found {0} small volumes(s) with a total size of {1}" , m_smallvolumes . Count (), Library . Utility . Utility . FormatSizeString ( m_smallspace ));
Logging . Log . WriteVerboseMessage ( LOGTAG , "WastedSpaceVolumes" , "Found {0} volume(s) with a total of {1:F2}% wasted space ({2} of {3})" , m_wastevolumes . Count (), wastepercentage , Library . Utility . Utility . FormatSizeString ( m_wastedspace ), Library . Utility . Utility . FormatSizeString ( m_fullsize ));
2016-09-15 11:39:27 +02:00
if ( m_deletablevolumes > 0 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactReason" , "Compacting because there are {0} fully deletable volume(s)" , m_deletablevolumes );
2016-09-15 11:39:27 +02:00
else if ( wastepercentage >= m_wastethreshold && m_wastevolumes . Count () >= 2 )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactReason" , "Compacting because there is {0:F2}% wasted space and the limit is {1}%" , wastepercentage , m_wastethreshold );
2016-09-15 11:39:27 +02:00
else if ( m_smallspace > m_volsize )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactReason" , "Compacting because there are {0} in small volumes and the volume size is {1}" , Library . Utility . Utility . FormatSizeString ( m_smallspace ), Library . Utility . Utility . FormatSizeString ( m_volsize ));
2013-08-24 21:16:44 +02:00
else if ( m_smallvolumecount > m_maxsmallfilecount )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactReason" , "Compacting because there are {0} small volumes and the maximum is {1}" , m_smallvolumecount , m_maxsmallfilecount );
2016-09-15 11:39:27 +02:00
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "CompactReason" , "Compacting not required" );
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets a value indicating whether reclamation should be performed.
/// </summary>
2016-09-15 11:39:27 +02:00
public bool ShouldReclaim
{
2024-11-01 16:45:25 +01:00
get
2016-09-15 11:39:27 +02:00
{
return m_deletablevolumes > 0 ;
}
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets a value indicating whether compaction should be performed.
/// </summary>
2016-09-15 11:39:27 +02:00
public bool ShouldCompact
{
2024-11-01 16:45:25 +01:00
get
2016-09-15 11:39:27 +02:00
{
return ((( m_wastedspace / ( float ) m_fullsize ) * 100 ) >= m_wastethreshold && m_wastevolumes . Count () >= 2 ) || m_smallspace > m_volsize || m_smallvolumecount > m_maxsmallfilecount ;
}
}
2013-04-07 09:10:04 +02:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the volumes that can be deleted.
/// </summary>
2024-11-01 16:45:25 +01:00
public IEnumerable < string > DeleteableVolumes
{
get { return from n in m_cleandelete select n . Name ; }
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets the volumes that can be compacted.
/// </summary>
2024-11-01 16:45:25 +01:00
public IEnumerable < string > CompactableVolumes
{
get
{
2016-09-15 11:39:27 +02:00
//The order matters, we compact old volumes together first,
// as we anticipate old data will stay around, where never data
// is more likely to be discarded again
return m_wastevolumes . Union ( m_smallvolumes ). Select ( x => x . Name ). Distinct ();
2024-11-01 16:45:25 +01:00
}
2016-09-15 11:39:27 +02:00
}
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Gets a compact report on the volumes that can be reclaimed or compacted.
/// </summary>
/// <param name="volsize">The size of the volume to trigger compaction.</param>
/// <param name="wastethreshold">The threshold for wasted space percentage to trigger compaction.</param>
/// <param name="smallfilesize">The size of small files to trigger compaction.</param>
/// <param name="maxsmallfilecount">The maximum number of small files to trigger compaction.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when awaited contains an instance of <see cref="ICompactReport"/>.</returns>
2025-06-18 16:07:46 +02:00
public async Task < ICompactReport > GetCompactReport ( long volsize , long wastethreshold , long smallfilesize , long maxsmallfilecount , CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-06-12 08:34:29 +02:00
return new CompactReport (
volsize ,
wastethreshold ,
smallfilesize ,
maxsmallfilecount ,
2025-06-18 16:07:46 +02:00
await GetWastedSpaceReport ( token )
. ToListAsync ( cancellationToken : token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false )
);
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Registers a block as duplicated to a new volume.
/// This is used when a block is moved to a new volume, and we want to keep track of it.
/// </summary>
2016-09-15 11:39:27 +02:00
public interface IBlockQuery : IDisposable
{
2024-11-01 16:45:25 +01:00
/// <summary>
2025-06-17 18:01:12 +02:00
/// Checks if a block is in use. If volumeId is not -1, check specific volume.
2024-11-01 16:45:25 +01:00
/// </summary>
2025-06-17 18:01:12 +02:00
/// <param name="hash">The hash of the block.</param>
/// <param name="size">The size of the block.</param>
/// <param name="volumeId">The volume ID to check, or -1 to check all volumes.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when awaited returns true if the block is in use, false otherwise.</returns>
2025-06-18 16:07:46 +02:00
Task < bool > UseBlock ( string hash , long size , long volumeId , CancellationToken token );
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// A helper class to query blocks in the database.
/// </summary>
2016-09-15 11:39:27 +02:00
private class BlockQuery : IBlockQuery
{
2025-06-17 18:01:12 +02:00
/// <summary>
/// The database instance to use for the queries.
/// </summary>
2025-05-19 21:51:28 +02:00
private LocalDatabase m_db = null !;
2025-06-17 18:01:12 +02:00
/// <summary>
/// The command used to query blocks in the database.
/// </summary>
2025-05-19 21:51:28 +02:00
private SqliteCommand m_command = null !;
2016-12-16 17:30:39 +01:00
2025-05-19 21:51:28 +02:00
[Obsolete("Calling this constructor will throw an exception. Use CreateAsync() instead.")]
public BlockQuery ( SqliteConnection connection , SqliteTransaction ? transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-19 21:51:28 +02:00
throw new NotImplementedException ( "Use CreateAsync() instead" );
}
2025-06-17 18:01:12 +02:00
/// <summary>
/// Constructs a new instance of the <see cref="BlockQuery"/> class.
/// It is private to prevent instantiation without CreateAsync.
/// </summary>
private BlockQuery () { }
2025-05-19 21:51:28 +02:00
2025-06-17 18:01:12 +02:00
/// <summary>
/// Creates a new instance of the <see cref="BlockQuery"/> class.
/// </summary>
/// <param name="db">The local database to use for the queries.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when awaited returns a new instance of <see cref="BlockQuery"/>.</returns>
2025-06-18 16:07:46 +02:00
public static async Task < BlockQuery > CreateAsync ( LocalDatabase db , CancellationToken token )
2025-05-19 21:51:28 +02:00
{
return new BlockQuery
{
m_db = db ,
m_command = await db . Connection . CreateCommandAsync ( @"
SELECT ""VolumeID""
FROM ""Block""
WHERE
""Hash"" = @Hash
AND ""Size"" = @Size
2025-06-18 16:07:46 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false )
2025-05-19 21:51:28 +02:00
};
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
/// <inheritdoc />
2025-06-18 16:07:46 +02:00
public async Task < bool > UseBlock ( string hash , long size , long volumeId , CancellationToken token )
2024-11-01 16:45:25 +01:00
{
2025-05-19 21:51:28 +02:00
var r = await m_command
. SetTransaction ( m_db . Transaction )
2025-04-04 14:41:58 +02:00
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:07:46 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2024-11-01 16:45:25 +01:00
if ( r == - 1 )
{
return false ;
}
else if ( volumeId == - 1 )
{
return true ;
}
else
{
// Check that the volume id matches
2025-04-01 15:54:54 +02:00
return r == volumeId ;
2024-11-01 16:45:25 +01:00
}
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2016-09-15 11:39:27 +02:00
public void Dispose ()
{
if ( m_command != null )
try { m_command . Dispose (); }
2025-04-03 15:24:57 +02:00
finally { m_command = null !; }
2016-09-15 11:39:27 +02:00
}
}
2024-11-01 16:45:25 +01:00
2016-09-15 11:39:27 +02:00
/// <summary>
2025-06-17 18:01:12 +02:00
/// Builds a lookup table to enable faster response to block queries.
2016-09-15 11:39:27 +02:00
/// </summary>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
public async Task < IBlockQuery > CreateBlockQueryHelper ( CancellationToken token )
2025-05-21 14:51:22 +02:00
{
2025-06-18 16:07:46 +02:00
return await BlockQuery . CreateAsync ( this , token ). ConfigureAwait ( false );
2025-05-21 14:51:22 +02:00
}
2013-03-27 16:06:45 +01:00
2024-11-01 14:50:26 +01:00
/// <summary>
2025-06-17 18:01:12 +02:00
/// Registers a block as moved to a new volume.
2024-11-01 14:50:26 +01:00
/// </summary>
2025-06-17 18:01:12 +02:00
/// <param name="hash">The hash of the block.</param>
/// <param name="size">The size of the block.</param>
/// <param name="volumeID">The new volume ID.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when completed indicates the block has been registered.</returns>
2025-06-18 16:07:46 +02:00
public async Task RegisterDuplicatedBlock ( string hash , long size , long volumeID , CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2024-11-01 14:50:26 +01:00
// Using INSERT OR IGNORE to avoid duplicate entries, result may be 1 or 0
2025-05-19 21:51:28 +02:00
await m_registerDuplicateBlockCommand
. SetTransaction ( m_rtr )
. SetParameterValue ( "@VolumeId" , volumeID )
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2024-11-01 14:50:26 +01:00
}
/// <summary>
2025-06-17 18:01:12 +02:00
/// After new volumes are uploaded, this method will update the blocks from the old volumes to point to the new volumes.
2024-11-01 14:50:26 +01:00
/// </summary>
2025-06-17 18:01:12 +02:00
/// <param name="filename">The file to remove.</param>
/// <param name="volumeIdsToBeRemoved">The volume IDs that will be removed.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>A task that when completed indicates the operation has finished.</returns>
2025-06-18 16:07:46 +02:00
public async Task PrepareForDelete ( string filename , IEnumerable < long > volumeIdsToBeRemoved , CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-06-18 16:07:46 +02:00
var deletedVolume = await GetRemoteVolume ( filename , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2024-11-01 14:50:26 +01:00
if ( deletedVolume . Type != RemoteVolumeType . Blocks )
return ;
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var updatedBlocks = "BlocksToUpdate-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
var replacementBlocks = "ReplacementBlocks-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
try
2024-11-01 14:50:26 +01:00
{
2025-06-18 11:18:25 +02:00
await cmd . SetCommandAndParameters ( $@"
2025-05-19 21:51:28 +02:00
CREATE {TEMPORARY} TABLE ""{updatedBlocks}"" AS
SELECT ""ID""
FROM ""Block""
WHERE ""VolumeID"" = @VolumeId
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@VolumeId" , deletedVolume . ID )
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-06-12 08:34:29 +02:00
2025-06-18 16:07:46 +02:00
await using ( var tempTable = await TemporaryDbValueList . CreateAsync ( this , volumeIdsToBeRemoved , token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
await (
await cmd . SetCommandAndParameters ( $@"
2025-06-12 08:34:29 +02:00
CREATE {TEMPORARY} TABLE ""{replacementBlocks}"" AS
SELECT
""BlockID"",
MAX(""VolumeID"") AS ""VolumeID""
FROM ""DuplicateBlock""
WHERE
""VolumeID"" NOT IN (@VolumeIds)
AND ""BlockID"" IN (
SELECT ""ID""
FROM ""{updatedBlocks}""
)
GROUP BY ""BlockID""
" )
2025-06-18 16:07:46 +02:00
. ExpandInClauseParameterMssqliteAsync ( "@VolumeIds" , tempTable , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false )
)
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
var targetCount = await cmd . ExecuteScalarInt64Async ( $@"
2025-05-19 21:51:28 +02:00
SELECT COUNT(*)
FROM ""{updatedBlocks}""
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-02 13:49:21 +02:00
2025-06-18 11:18:25 +02:00
if ( targetCount == 0 )
return ;
2024-11-01 14:50:26 +01:00
2025-06-18 11:18:25 +02:00
var replacementCount = await cmd . ExecuteScalarInt64Async ( $@"
2025-05-19 21:51:28 +02:00
SELECT COUNT(*)
FROM ""{replacementBlocks}""
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
var updateCount = await cmd . SetCommandAndParameters ( @ $"
2025-05-19 21:51:28 +02:00
UPDATE ""Block""
SET ""VolumeID"" = (
SELECT ""VolumeID""
FROM ""{replacementBlocks}""
WHERE
""{replacementBlocks}"".""BlockID"" = ""Block"".""ID""
AND ""Block"".""VolumeID"" = @VolumeId
)
WHERE ""Block"".""VolumeID"" = @VolumeId
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@VolumeId" , deletedVolume . ID )
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
var deleteCount = await cmd . ExecuteNonQueryAsync ( @ $"
2025-05-19 21:51:28 +02:00
DELETE FROM ""DuplicateBlock""
WHERE
(
""DuplicateBlock"".""BlockID""
|| ':'
|| ""DuplicateBlock"".""VolumeID""
) IN (
SELECT
""RB"".""BlockID""
|| ':'
|| ""RB"".""VolumeID""
2025-06-18 10:42:08 +02:00
FROM ""{replacementBlocks}"" ""RB""
2025-05-19 21:51:28 +02:00
)
2025-06-18 16:07:46 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 21:51:28 +02:00
2025-06-18 11:18:25 +02:00
if ( targetCount != updateCount
|| replacementCount != deleteCount
|| updateCount != deleteCount )
{
throw new Exception ( $"Unexpected number of rows updated. Expected {targetCount} but got updated {updateCount}, deleted {deleteCount}, and replaced {replacementCount}" );
}
2024-11-01 14:50:26 +01:00
2025-06-18 11:18:25 +02:00
// Remove knowledge of any old blocks
await cmd . SetCommandAndParameters ( @ $"
2025-05-19 21:51:28 +02:00
DELETE FROM ""DuplicateBlock""
WHERE ""VolumeID"" = @VolumeId
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@VolumeId" , deletedVolume . ID )
2025-06-18 16:07:46 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
}
finally
{
try
{
2025-06-18 16:07:46 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{updatedBlocks}"" " , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2024-11-01 14:50:26 +01:00
}
2025-06-18 11:18:25 +02:00
catch { }
try
2024-11-01 14:50:26 +01:00
{
2025-06-18 16:07:46 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{replacementBlocks}"" " , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2024-11-01 14:50:26 +01:00
}
2025-06-18 11:18:25 +02:00
catch { }
2024-11-01 14:50:26 +01:00
}
2016-09-15 11:39:27 +02:00
}
2024-11-01 16:45:25 +01:00
2016-09-15 11:39:27 +02:00
/// <summary>
2017-01-05 09:57:06 +01:00
/// Calculates the sequence in which files should be deleted based on their relations.
2016-09-15 11:39:27 +02:00
/// </summary>
/// <param name="deleteableVolumes">Block volumes slated for deletion.</param>
2025-06-18 16:07:46 +02:00
/// <param name="token">A cancellation token to cancel the operation.</param>
2025-06-17 18:01:12 +02:00
/// <returns>An asynchronous enumerable of <see cref="IRemoteVolume"/> that represents the order in which volumes should be deleted.</returns>
2025-06-18 16:07:46 +02:00
public async IAsyncEnumerable < IRemoteVolume > ReOrderDeleteableVolumes ( IEnumerable < IRemoteVolume > deleteableVolumes , [ EnumeratorCancellation ] CancellationToken token )
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
// Although the generated index volumes are always in pairs,
// this code handles many-to-many relations between
// index files and block volumes, should this be added later
var lookupBlock = new Dictionary < string , List < IRemoteVolume >>();
var lookupIndexfiles = new Dictionary < string , List < string >>();
cmd . SetCommandAndParameters ( @"
2025-05-19 21:51:28 +02:00
SELECT
""C"".""Name"",
""B"".""Name"",
""B"".""Hash"",
""B"".""Size""
FROM
""IndexBlockLink"" A,
""RemoteVolume"" B,
""RemoteVolume"" C
WHERE
""A"".""IndexVolumeID"" = ""B"".""ID""
AND ""A"".""BlockVolumeID"" = ""C"".""ID""
AND ""B"".""Hash"" IS NOT NULL
AND ""B"".""Size"" IS NOT NULL
" );
2025-06-18 16:07:46 +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
{
var name = rd . ConvertValueToString ( 0 ) ?? "" ;
if (! lookupBlock . TryGetValue ( name , out var indexfileList ))
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:18:25 +02:00
indexfileList = new List < IRemoteVolume >();
lookupBlock . Add ( name , indexfileList );
}
2024-11-01 16:45:25 +01:00
2025-06-18 11:18:25 +02:00
var v = new RemoteVolume (
rd . ConvertValueToString ( 1 ),
rd . ConvertValueToString ( 2 ),
rd . ConvertValueToInt64 ( 3 )
);
indexfileList . Add ( v );
2013-04-08 22:24:54 +02:00
2025-06-18 11:18:25 +02:00
if (! lookupIndexfiles . TryGetValue ( v . Name , out var blockList ))
{
blockList = new List < string >();
lookupIndexfiles . Add ( v . Name , blockList );
2016-09-15 11:39:27 +02:00
}
2025-06-18 11:18:25 +02:00
blockList . Add ( name );
}
2013-04-08 22:24:54 +02:00
2025-06-18 11:18:25 +02:00
foreach ( var r in deleteableVolumes . Distinct ())
{
// Return the input
yield return r ;
if ( lookupBlock . TryGetValue ( r . Name , out var indexfileList ))
foreach ( var sh in indexfileList )
{
if ( lookupIndexfiles . TryGetValue ( sh . Name , out var backref ))
2016-09-15 11:39:27 +02:00
{
2025-06-18 11:18:25 +02:00
//If this is the last reference,
// remove the index file as well
if ( backref . Remove ( r . Name ) && backref . Count == 0 )
yield return sh ;
2016-09-15 11:39:27 +02:00
}
2025-06-18 11:18:25 +02:00
}
2016-09-15 11:39:27 +02:00
}
}
2013-03-27 16:06:45 +01:00
2016-09-15 11:39:27 +02:00
}
2013-03-27 16:06:45 +01:00
}