2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-01-28 08:54:50 +01:00
// https://duplicati.com, hello@duplicati.com
2025-05-21 15:37:46 +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-01-28 08:54:50 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-21 15:37:46 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-28 08:54:50 +01:00
// all copies or substantial portions of the Software.
2025-05-21 15:37:46 +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-05-16 14:14:06 +02:00
#nullable enable
2017-01-05 16:46:01 +01:00
using System ;
using System.Collections ;
using System.Collections.Generic ;
2025-03-14 10:41:14 +01:00
using System.IO ;
2017-01-05 16:46:01 +01:00
using System.Linq ;
2025-03-14 10:41:14 +01:00
using System.Threading.Tasks ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
2025-01-28 08:54:50 +01:00
using Duplicati.Library.Utility ;
2017-01-05 16:46:01 +01:00
namespace Duplicati.Library.Main.Operation
{
internal class ListBrokenFilesHandler
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( ListBrokenFilesHandler ));
2019-10-19 10:56:21 -07:00
protected readonly Options m_options ;
protected readonly ListBrokenFilesResults m_result ;
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
public ListBrokenFilesHandler ( Options options , ListBrokenFilesResults result )
2017-01-05 16:46:01 +01:00
{
m_options = options ;
m_result = result ;
}
2025-05-16 14:14:06 +02:00
public async Task RunAsync ( IBackendManager backendManager , IFilter filter , Func < long , DateTime , long , string , long , bool >? callbackhandler = null )
2017-01-05 16:46:01 +01:00
{
2025-03-14 10:41:14 +01:00
if (! File . Exists ( m_options . Dbpath ))
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( string . Format ( "Database file does not exist: {0}" , m_options . Dbpath ), "DatabaseDoesNotExist" );
2017-01-05 16:46:01 +01:00
2025-06-19 11:41:39 +02:00
await using var db = await Database . LocalListBrokenFilesDatabase . CreateAsync ( m_options . Dbpath , null , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-06-19 09:47:03 +02:00
await Utility . UpdateOptionsFromDb ( db , m_options , m_result . TaskControl . ProgressToken )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-06-19 09:47:03 +02:00
await Utility . VerifyOptionsAndUpdateDatabase ( db , m_options , m_result . TaskControl . ProgressToken )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
await DoRunAsync ( backendManager , db , filter , callbackhandler ). ConfigureAwait ( false );
await db . Transaction . RollBackAsync (). ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
}
2025-05-22 14:09:17 +02:00
public static async Task <(( DateTime FilesetTime , long FilesetID , long RemoveCount )[]?, List < Database . RemoteVolumeEntry >? Missing )> GetBrokenFilesetsFromRemote ( IBackendManager backendManager , BasicResults result , Database . LocalListBrokenFilesDatabase db , Options options )
2017-01-05 16:46:01 +01:00
{
2025-05-16 14:14:06 +02:00
List < Database . RemoteVolumeEntry >? missing = null ;
2025-06-12 08:34:29 +02:00
var brokensets = await db
2025-06-19 09:47:03 +02:00
. GetBrokenFilesets ( options . Time , options . Version , result . TaskControl . ProgressToken )
. ToArrayAsync ( cancellationToken : result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
if ( brokensets . Length == 0 )
{
2025-06-19 09:47:03 +02:00
if ( await db . RepairInProgress ( result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Cannot continue because the database is marked as being under repair, but does not have broken files." , "CannotListOnDatabaseInRepair" );
2017-01-05 16:46:01 +01:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenFilesetsInDatabase" , "No broken filesets found in database, checking for missing remote files" );
2017-01-05 16:46:01 +01:00
2025-06-19 09:47:03 +02:00
var remotestate = await FilelistProcessor . RemoteListAnalysis ( backendManager , options , db , result . BackendWriter , null , null , FilelistProcessor . VerifyMode . VerifyOnly , result . TaskControl . ProgressToken ). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
if (! remotestate . ParsedVolumes . Any ())
throw new UserInformationException ( "No remote volumes were found, refusing purge" , "CannotPurgeWithNoRemoteVolumes" );
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
missing = remotestate . MissingVolumes . ToList ();
if ( missing . Count == 0 )
{
Logging . Log . WriteInformationMessage ( LOGTAG , "NoMissingFilesFound" , "Skipping operation because no files were found to be missing, and no filesets were recorded as broken." );
2025-03-14 10:41:14 +01:00
return ( null , missing );
2025-01-28 08:54:50 +01:00
}
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
// Mark all volumes as disposable
foreach ( var f in missing )
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. UpdateRemoteVolume ( f . Name , RemoteVolumeState . Deleting , f . Size , f . Hash , result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "MarkedRemoteFilesForDeletion" , "Marked {0} remote files for deletion" , missing . Count );
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
// Drop all content from tables
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. RemoveMissingBlocks ( missing . Select ( x => x . Name ), result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-09 11:12:29 +02:00
// Mark all orphaned index files as disposable after removing the missing block files
2025-06-19 09:47:03 +02:00
await foreach ( var f in db . GetOrphanedIndexFiles ( result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2025-06-12 08:34:29 +02:00
await db
2025-06-19 09:47:03 +02:00
. UpdateRemoteVolume ( f . Name , RemoteVolumeState . Deleting , f . Size , f . Hash , result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
brokensets = await db
2025-06-19 09:47:03 +02:00
. GetBrokenFilesets ( options . Time , options . Version , result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ToArrayAsync ()
. ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
}
2025-03-14 10:41:14 +01:00
return ( brokensets , missing );
2017-01-05 16:46:01 +01:00
}
2025-05-22 14:09:17 +02:00
private async Task DoRunAsync ( IBackendManager backendManager , Database . LocalListBrokenFilesDatabase db , IFilter filter , Func < long , DateTime , long , string , long , bool >? callbackhandler )
2017-01-05 16:46:01 +01:00
{
if ( filter != null && ! filter . Empty )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Filters are not supported for this operation" , "FiltersAreNotSupportedForListBrokenFiles" );
2017-01-05 16:46:01 +01:00
2025-06-19 09:47:03 +02:00
if ( await db . PartiallyRecreated ( m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "The command does not work on partially recreated databases" , "ListBrokenFilesDoesNotWorkOnPartialDatabase" );
2017-01-05 16:46:01 +01:00
2025-05-21 15:43:08 +02:00
( var brokensets , var missing ) = await GetBrokenFilesetsFromRemote ( backendManager , m_result , db , m_options ). ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
if ( brokensets == null )
return ;
if ( brokensets . Length == 0 )
{
2025-03-14 10:41:14 +01:00
m_result . BrokenFiles = [];
2018-07-09 20:38:41 +02:00
if ( missing == null )
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenFilesets" , "Found no broken filesets" );
else if ( missing . Count == 0 )
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenFilesetsOrMissingFiles" , "Found no broken filesets and no missing remote files" );
else
Logging . Log . WriteInformationMessage ( LOGTAG , "NoBrokenSetsButMissingRemoteFiles" , string . Format ( "Found no broken filesets, but {0} missing remote files. Run purge-broken-files." , missing . Count ));
2017-01-05 16:46:01 +01:00
return ;
}
2025-06-12 08:34:29 +02:00
var fstimes = await db
2025-06-19 09:47:03 +02:00
. FilesetTimes ( m_result . TaskControl . ProgressToken )
. ToListAsync ( cancellationToken : m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
2025-01-28 08:54:50 +01:00
var brokenfilesets =
brokensets . Select ( x => new
2017-01-05 16:46:01 +01:00
{
2025-05-21 15:43:08 +02:00
Version = fstimes . FindIndex ( y => y . Key == x . FilesetID ),
Timestamp = x . FilesetTime ,
FilesetID = x . FilesetID ,
BrokenCount = x . RemoveCount
2017-01-05 16:46:01 +01:00
}
)
. ToArray ();
m_result . BrokenFiles =
2025-05-27 06:41:35 +02:00
await Task . WhenAll ( brokenfilesets . Select (
async x => new Tuple < long , DateTime , IEnumerable < Tuple < string , long >>>(
2025-01-28 08:54:50 +01:00
x . Version ,
x . Timestamp ,
2017-01-05 16:46:01 +01:00
callbackhandler == null && ! m_options . ListSetsOnly
2025-06-12 08:34:29 +02:00
? await db
2025-06-19 09:47:03 +02:00
. GetBrokenFilenames ( x . FilesetID , m_result . TaskControl . ProgressToken )
. ToArrayAsync ( cancellationToken : m_result . TaskControl . ProgressToken )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false )
2017-01-05 16:46:01 +01:00
: new MockList < Tuple < string , long >>(( int ) x . BrokenCount )
2025-06-12 08:34:29 +02:00
)))
. ConfigureAwait ( false );
2017-01-05 16:46:01 +01:00
if ( callbackhandler != null )
foreach ( var bs in brokenfilesets )
2025-06-19 09:47:03 +02:00
await foreach ( var fe in db . GetBrokenFilenames ( bs . FilesetID , m_result . TaskControl . ProgressToken ). ConfigureAwait ( false ))
2017-01-05 16:46:01 +01:00
if (! callbackhandler ( bs . Version , bs . Timestamp , bs . BrokenCount , fe . Item1 , fe . Item2 ))
break ;
}
private class MockList < T > : IList < T >
{
2025-05-16 15:23:02 +02:00
private readonly List < T > _dummy = new List < T >();
2017-01-05 16:46:01 +01:00
public MockList ( int count )
{
Count = count ;
}
public T this [ int index ]
{
get { throw new NotImplementedException (); }
set { throw new NotImplementedException (); }
}
public int Count { get ; private set ; }
2025-01-28 08:54:50 +01:00
public bool IsReadOnly => true ;
2017-01-05 16:46:01 +01:00
public void Add ( T item )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public void Clear ()
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public bool Contains ( T item )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public void CopyTo ( T [] array , int arrayIndex )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public IEnumerator < T > GetEnumerator ()
2025-05-16 15:23:02 +02:00
=> _dummy . GetEnumerator ();
2017-01-05 16:46:01 +01:00
public int IndexOf ( T item )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public void Insert ( int index , T item )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public bool Remove ( T item )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
public void RemoveAt ( int index )
2025-05-16 15:23:02 +02:00
=> throw new NotImplementedException ();
2017-01-05 16:46:01 +01:00
IEnumerator IEnumerable . GetEnumerator ()
2025-05-16 15:23:02 +02:00
=> _dummy . GetEnumerator ();
2017-01-05 16:46:01 +01:00
}
}
}