2025-01-28 08:54:50 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// 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-12-18 08:27:59 +01:00
// DEALINGS IN THE SOFTWARE.
2013-03-31 19:33:34 +02:00
using System.Collections.Generic ;
using System.Linq ;
2025-01-28 08:54:50 +01:00
using System.Threading ;
using System.Threading.Tasks ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Interface ;
2019-08-31 14:44:12 -04:00
using Duplicati.Library.Main.Database ;
2019-11-20 20:14:15 -08:00
using Duplicati.Library.Main.Volumes ;
2019-09-29 20:16:28 -07:00
using Duplicati.Library.Utility ;
2013-03-31 19:33:34 +02:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Operation
2019-08-05 20:14:05 -04:00
{
2013-05-25 16:40:15 +02:00
internal class ListFilesHandler
2019-08-05 20:14:05 -04:00
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < ListFilesHandler >();
2018-05-23 21:18:01 -07:00
private readonly Options m_options ;
private readonly ListResults m_result ;
2013-03-31 19:33:34 +02:00
2025-01-28 08:54:50 +01:00
public ListFilesHandler ( Options options , ListResults result )
2013-03-31 19:33:34 +02:00
{
m_options = options ;
2013-05-25 16:40:15 +02:00
m_result = result ;
2013-03-31 19:33:34 +02:00
}
2025-01-28 08:54:50 +01:00
public async Task Run ( IBackendManager backendManager , IEnumerable < string > filterstrings , IFilter compositefilter )
2013-05-25 16:40:15 +02:00
{
2025-01-28 08:54:50 +01:00
var cancellationToken = m_result . TaskControl . ProgressToken ;
var parsedfilter = new FilterExpression ( filterstrings );
var filter = JoinedFilterExpression . Join ( parsedfilter , compositefilter );
var simpleList = !(( filter is FilterExpression expression && expression . Type == FilterType . Simple ) || m_options . AllVersions );
2019-08-05 20:14:05 -04:00
2013-03-31 19:33:34 +02:00
//Use a speedy local query
2013-05-13 22:32:05 +02:00
if (! m_options . NoLocalDb && System . IO . File . Exists ( m_options . Dbpath ))
2019-08-05 20:14:05 -04:00
using ( var db = new Database . LocalListDatabase ( m_options . Dbpath ))
2013-05-25 16:40:15 +02:00
{
m_result . SetDatabase ( db );
2019-08-05 20:14:05 -04:00
using ( var filesets = db . SelectFileSets ( m_options . Time , m_options . Version ))
2013-05-11 12:03:15 +02:00
{
2017-04-06 11:54:22 +02:00
if (! filter . Empty )
2014-03-21 15:01:49 +01:00
{
if ( simpleList || ( m_options . ListFolderContents && ! m_options . AllVersions ))
2019-08-05 20:14:05 -04:00
{
2014-03-21 15:01:49 +01:00
filesets . TakeFirst ();
2019-08-05 20:14:05 -04:00
}
2014-03-21 15:01:49 +01:00
}
IEnumerable < Database . LocalListDatabase . IFileversion > files ;
2014-03-21 19:23:54 +01:00
if ( m_options . ListFolderContents )
2019-08-05 20:14:05 -04:00
{
2014-03-21 15:01:49 +01:00
files = filesets . SelectFolderContents ( filter );
2019-08-05 20:14:05 -04:00
}
2014-03-21 15:01:49 +01:00
else if ( m_options . ListPrefixOnly )
2019-08-05 20:14:05 -04:00
{
2014-03-21 15:01:49 +01:00
files = filesets . GetLargestPrefix ( filter );
2019-08-05 20:14:05 -04:00
}
2017-04-06 11:54:22 +02:00
else if ( filter . Empty )
2019-08-05 20:14:05 -04:00
{
2014-03-21 19:23:54 +01:00
files = null ;
2019-08-05 20:14:05 -04:00
}
2014-03-21 15:01:49 +01:00
else
2019-08-05 20:14:05 -04:00
{
2014-03-21 15:01:49 +01:00
files = filesets . SelectFiles ( filter );
2019-08-05 20:14:05 -04:00
}
2014-08-19 20:24:54 +02:00
if ( m_options . ListSetsOnly )
2019-08-05 20:14:05 -04:00
{
2014-08-19 20:24:54 +02:00
m_result . SetResult (
2019-08-05 20:14:05 -04:00
filesets . QuickSets . Select ( x => new ListResultFileset ( x . Version , x . IsFullBackup , x . Time , x . FileCount , x . FileSizes )). ToArray (),
2014-08-19 20:24:54 +02:00
null
);
2019-08-05 20:14:05 -04:00
}
2014-08-19 20:24:54 +02:00
else
2019-08-05 20:14:05 -04:00
{
2014-08-19 20:24:54 +02:00
m_result . SetResult (
2019-08-05 20:14:05 -04:00
filesets . Sets . Select ( x =>
new ListResultFileset ( x . Version , x . IsFullBackup , x . Time , x . FileCount , x . FileSizes )). ToArray (),
files == null
? null
: ( from n in files
select ( Duplicati . Library . Interface . IListResultFile )( new ListResultFile ( n . Path ,
n . Sizes . ToArray ())))
. ToArray ()
2014-08-19 20:24:54 +02:00
);
2019-08-05 20:14:05 -04:00
}
2013-05-25 16:40:15 +02:00
return ;
2013-05-11 12:03:15 +02:00
}
2013-05-25 16:40:15 +02:00
}
2019-08-05 20:14:05 -04:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "NoLocalDatabase" , "No local database, accessing remote store" );
2016-04-19 10:04:22 +02:00
2014-03-21 15:01:49 +01:00
//TODO: Add prefix and foldercontents
2016-04-19 10:04:22 +02:00
if ( m_options . ListFolderContents )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Listing folder contents is not supported without a local database, consider using the \"repair\" option to rebuild the database." , "FolderContentListingRequiresLocalDatabase" );
2016-04-19 10:04:22 +02:00
else if ( m_options . ListPrefixOnly )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "Listing prefixes is not supported without a local database, consider using the \"repair\" option to rebuild the database." , "PrefixListingRequiresLocalDatabase" );
2013-04-01 13:05:55 +02:00
2013-03-31 19:33:34 +02:00
// Otherwise, grab info from remote location
2025-01-28 08:54:50 +01:00
using ( var tmpdb = new TempFile ())
using ( var db = new LocalDatabase ( tmpdb , "List" , true ))
2013-03-31 19:33:34 +02:00
{
2013-05-25 16:40:15 +02:00
m_result . SetDatabase ( db );
2019-08-05 20:14:05 -04:00
2025-01-28 08:54:50 +01:00
var filteredList = ParseAndFilterFilesets ( await backendManager . ListAsync ( cancellationToken ). ConfigureAwait ( false ), m_options );
2013-05-11 12:03:15 +02:00
if ( filteredList . Count == 0 )
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( "No filesets found on remote target" , "EmptyRemoteFolder" );
2013-05-30 21:54:43 +02:00
2025-01-28 08:54:50 +01:00
var numberSeq = await CreateResultSequence ( filteredList , backendManager , m_options , cancellationToken );
2017-04-06 11:54:22 +02:00
if ( filter . Empty )
2013-05-25 16:40:15 +02:00
{
m_result . SetResult ( numberSeq , null );
2016-02-10 17:18:48 +01:00
m_result . EncryptedFiles = filteredList . Any ( x => ! string . IsNullOrWhiteSpace ( x . Value . EncryptionModule ));
2013-05-25 16:40:15 +02:00
return ;
}
2019-08-05 20:14:05 -04:00
2013-05-11 12:03:15 +02:00
var firstEntry = filteredList [ 0 ]. Value ;
filteredList . RemoveAt ( 0 );
2019-08-05 20:14:05 -04:00
Dictionary < string , List < long >> res ;
2025-01-28 08:54:50 +01:00
if (! await m_result . TaskControl . ProgressRendevouz (). ConfigureAwait ( false ))
2014-05-15 12:47:16 +02:00
return ;
2019-08-05 20:14:05 -04:00
2025-01-28 08:54:50 +01:00
using ( var tmpfile = await backendManager . GetAsync ( firstEntry . File . Name , null , firstEntry . File . Size , cancellationToken ). ConfigureAwait ( false ))
using ( var rd = new FilesetVolumeReader ( RestoreHandler . GetCompressionModule ( firstEntry . File . Name ), tmpfile , m_options ))
2013-05-11 12:03:15 +02:00
if ( simpleList )
{
2013-05-25 16:40:15 +02:00
m_result . SetResult (
numberSeq . Take ( 1 ),
2013-05-11 12:03:15 +02:00
( from n in rd . Files
2019-08-05 20:14:05 -04:00
where Library . Utility . FilterExpression . Matches ( filter , n . Path )
orderby n . Path
select new ListResultFile ( n . Path , new long [] { n . Size }))
2013-05-11 12:03:15 +02:00
. ToArray ()
2013-05-25 16:40:15 +02:00
);
2019-08-05 20:14:05 -04:00
2013-05-25 16:40:15 +02:00
return ;
2013-05-11 12:03:15 +02:00
}
else
{
res = rd . Files
2013-06-30 12:42:13 +02:00
. Where ( x => Library . Utility . FilterExpression . Matches ( filter , x . Path ))
2013-05-11 12:03:15 +02:00
. ToDictionary (
2019-08-05 20:14:05 -04:00
x => x . Path ,
y =>
{
2013-05-11 12:03:15 +02:00
var lst = new List < long >();
lst . Add ( y . Size );
return lst ;
},
Library . Utility . Utility . ClientFilenameStringComparer
);
}
2019-08-05 20:14:05 -04:00
2013-05-11 12:03:15 +02:00
long flindex = 1 ;
2025-01-28 08:54:50 +01:00
var filteredListMap = filteredList . ToDictionary ( x => x . Value . File . Name , x => x . Value );
2025-02-18 09:18:36 +01:00
await foreach ( var ( tmpfile , hash , size , name ) in backendManager . GetFilesOverlappedAsync ( filteredList . Select ( x => new RemoteVolumeMapper ( x . Value )), cancellationToken ). ConfigureAwait ( false ))
2025-01-28 08:54:50 +01:00
{
var flentry = filteredListMap [ name ];
using ( tmpfile )
using ( var rd = new FilesetVolumeReader ( flentry . CompressionModule , tmpfile , m_options ))
2013-05-11 12:03:15 +02:00
{
2025-01-28 08:54:50 +01:00
if (! await m_result . TaskControl . ProgressRendevouz (). ConfigureAwait ( false ))
2014-05-15 12:47:16 +02:00
return ;
2019-08-05 20:14:05 -04:00
foreach ( var p in from n in rd . Files where Library . Utility . FilterExpression . Matches ( filter , n . Path ) select n )
2013-05-11 12:03:15 +02:00
{
List < long > lst ;
if (! res . TryGetValue ( p . Path , out lst ))
{
lst = new List < long >();
res [ p . Path ] = lst ;
2019-08-05 20:14:05 -04:00
for ( var i = 0 ; i < flindex ; i ++)
2013-05-11 12:03:15 +02:00
lst . Add (- 1 );
}
2019-08-05 20:14:05 -04:00
2013-05-11 12:03:15 +02:00
lst . Add ( p . Size );
}
2019-08-05 20:14:05 -04:00
foreach ( var n in from i in res where i . Value . Count < flindex + 1 select i )
2013-05-11 12:03:15 +02:00
n . Value . Add (- 1 );
2019-08-05 20:14:05 -04:00
2013-05-11 12:03:15 +02:00
flindex ++;
}
2025-01-28 08:54:50 +01:00
}
2019-08-05 20:14:05 -04:00
2013-05-25 16:40:15 +02:00
m_result . SetResult (
2013-05-11 12:03:15 +02:00
numberSeq ,
from n in res
orderby n . Key
2013-05-25 16:40:15 +02:00
select ( Duplicati . Library . Interface . IListResultFile )( new ListResultFile ( n . Key , n . Value ))
);
2013-03-31 19:33:34 +02:00
}
}
2013-05-30 21:54:43 +02:00
public static List < KeyValuePair < long , Volumes . IParsedVolume >> ParseAndFilterFilesets ( IEnumerable < Duplicati . Library . Interface . IFileEntry > rawlist , Options options )
{
var parsedlist = ( from n in rawlist
2019-08-05 20:14:05 -04:00
let p = Volumes . VolumeBase . ParseFilename ( n )
where p != null && p . FileType == RemoteVolumeType . Files
orderby p . Time descending
select p ). ToArray ();
2013-05-30 21:54:43 +02:00
var filelistFilter = RestoreHandler . FilterNumberedFilelist ( options . Time , options . Version );
2019-08-05 20:14:05 -04:00
return filelistFilter ( parsedlist ). ToList ();
2013-05-30 21:54:43 +02:00
}
2019-08-05 20:14:05 -04:00
2025-01-28 08:54:50 +01:00
private class RemoteVolumeMapper ( IParsedVolume Volume ) : IRemoteVolume
{
public string Name => Volume . File . Name ;
public string Hash => null ;
public long Size => Volume . File . Size ;
}
private static async Task < IEnumerable < IListResultFileset >> CreateResultSequence ( IEnumerable < KeyValuePair < long , IParsedVolume >> filteredList , IBackendManager backendManager , Options options , CancellationToken cancelToken )
2013-05-30 21:54:43 +02:00
{
2025-01-28 08:54:50 +01:00
var list = new List < IListResultFileset >();
var map = filteredList . ToDictionary ( x => x . Value . File . Name , x => x );
2025-02-18 09:18:36 +01:00
await foreach ( var ( file , hash , size , name ) in backendManager . GetFilesOverlappedAsync ( filteredList . Select ( x => new RemoteVolumeMapper ( x . Value )), cancelToken ). ConfigureAwait ( false ))
2019-11-20 20:14:15 -08:00
{
2025-01-28 08:54:50 +01:00
// We must obtain the partial/full status from the fileset file in the dlist files.
// Without this, the restore dialog will show all versions as full, or all versions
// as partial. While the dlist files are already downloaded elsewhere, doing so again
// here is the most direct way to obtain the partial/full status without a major
// refactoring. Since restoring directly from the backend files should be a relatively
// rare event, we can work on improving the performance later.
using ( file )
2019-11-20 20:14:15 -08:00
{
2025-01-28 08:54:50 +01:00
var ent = map [ name ];
var filesetData = VolumeReaderBase . GetFilesetData ( ent . Value . CompressionModule , file , options );
list . Add ( new ListResultFileset ( ent . Key , filesetData . IsFullBackup ? BackupType . FULL_BACKUP : BackupType . PARTIAL_BACKUP , ent . Value . Time . ToLocalTime (), - 1 , - 1 ));
2019-11-20 20:14:15 -08:00
}
}
return list . ToArray ();
2019-08-05 20:14:05 -04:00
}
2013-03-31 19:33:34 +02:00
}
}