2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, 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
// DEALINGS IN THE SOFTWARE.
2016-01-28 22:24:51 +01:00
using System ;
using CoCoL ;
using System.Threading.Tasks ;
using System.IO ;
using System.Collections.Generic ;
using System.Linq ;
2019-07-23 10:35:33 -04:00
using System.Threading ;
2016-02-09 09:17:31 +01:00
using Duplicati.Library.Main.Operation.Common ;
2018-04-24 14:41:21 +02:00
using Duplicati.Library.Snapshots ;
2019-07-23 10:35:33 -04:00
using Duplicati.Library.Common.IO ;
2016-01-28 22:24:51 +01:00
namespace Duplicati.Library.Main.Operation.Backup
{
/// <summary>
/// The file enumeration process takes a list of source folders as input,
/// applies all filters requested and emits the filtered set of filenames
/// to its output channel
/// </summary>
2018-04-24 13:11:03 +02:00
internal static class FileEnumerationProcess
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
/// <summary>
/// The log tag to use
/// </summary>
private static readonly string FILTER_LOGTAG = Logging . Log . LogTagFromType ( typeof ( FileEnumerationProcess ));
2019-07-23 10:35:33 -04:00
public static Task Run ( IEnumerable < string > sources , Snapshots . ISnapshotService snapshot , UsnJournalService journalService , FileAttributes fileAttributes , Duplicati . Library . Utility . IFilter sourcefilter , Duplicati . Library . Utility . IFilter emitfilter , Options . SymlinkStrategy symlinkPolicy , Options . HardlinkStrategy hardlinkPolicy , bool excludeemptyfolders , string [] ignorenames , string [] changedfilelist , ITaskReader taskreader , CancellationToken token )
2016-02-27 02:15:42 +01:00
{
return AutomationExtensions . RunTask (
2018-04-24 13:11:03 +02:00
new
{
2016-02-27 02:15:42 +01:00
Output = Backup . Channels . SourcePaths . ForWrite
},
async self =>
{
2019-07-23 10:35:33 -04:00
if (! token . IsCancellationRequested )
{
var hardlinkmap = new Dictionary < string , string >();
var mixinqueue = new Queue < string >();
Duplicati . Library . Utility . IFilter enumeratefilter = emitfilter ;
2016-02-27 02:15:42 +01:00
2019-07-23 10:35:33 -04:00
bool includes ;
bool excludes ;
Library . Utility . FilterExpression . AnalyzeFilters ( emitfilter , out includes , out excludes );
if ( includes && ! excludes )
enumeratefilter = Library . Utility . FilterExpression . Combine ( emitfilter , new Duplicati . Library . Utility . FilterExpression ( "*" + System . IO . Path . DirectorySeparatorChar , true ));
2016-02-27 02:15:42 +01:00
2019-07-23 10:35:33 -04:00
// Simplify checking for an empty list
if ( ignorenames != null && ignorenames . Length == 0 )
ignorenames = null ;
2018-04-24 11:25:37 +02:00
2019-07-23 10:35:33 -04:00
// If we have a specific list, use that instead of enumerating the filesystem
IEnumerable < string > worklist ;
if ( changedfilelist != null && changedfilelist . Length > 0 )
2016-02-27 02:15:42 +01:00
{
2019-07-23 10:35:33 -04:00
worklist = changedfilelist . Where ( x =>
2016-02-27 02:15:42 +01:00
{
2019-07-23 10:35:33 -04:00
var fa = FileAttributes . Normal ;
try
{
fa = snapshot . GetAttributes ( x );
}
catch
{
}
2019-08-02 13:48:50 -04:00
if ( token . IsCancellationRequested )
{
return false ;
}
2019-07-24 15:58:44 -04:00
2019-07-23 10:35:33 -04:00
return AttributeFilter ( x , fa , snapshot , sourcefilter , hardlinkPolicy , symlinkPolicy , hardlinkmap , fileAttributes , enumeratefilter , ignorenames , mixinqueue );
});
}
else
{
Library . Utility . Utility . EnumerationFilterDelegate attributeFilter = ( root , path , attr ) =>
AttributeFilter ( path , attr , snapshot , sourcefilter , hardlinkPolicy , symlinkPolicy , hardlinkmap , fileAttributes , enumeratefilter , ignorenames , mixinqueue );
if ( journalService != null )
2016-02-27 02:15:42 +01:00
{
2019-07-23 10:35:33 -04:00
// filter sources using USN journal, to obtain a sub-set of files / folders that may have been modified
sources = journalService . GetModifiedSources ( attributeFilter );
2016-02-27 02:15:42 +01:00
}
2019-07-23 10:35:33 -04:00
worklist = snapshot . EnumerateFilesAndFolders ( sources , attributeFilter , ( rootpath , errorpath , ex ) =>
{
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "FileAccessError" , ex , "Error reported while accessing file: {0}" , errorpath );
});
2018-04-23 22:28:27 +02:00
}
2018-04-26 23:03:50 +02:00
2019-08-02 13:48:50 -04:00
if ( token . IsCancellationRequested )
{
return ;
}
2019-07-24 15:58:44 -04:00
2019-07-23 10:35:33 -04:00
var source = ExpandWorkList ( worklist , mixinqueue , emitfilter , enumeratefilter );
if ( excludeemptyfolders )
source = ExcludeEmptyFolders ( source );
2016-01-28 22:24:51 +01:00
2019-07-23 10:35:33 -04:00
// Process each path, and dequeue the mixins with symlinks as we go
foreach ( var s in source )
{
2019-08-02 13:48:50 -04:00
if ( token . IsCancellationRequested )
{
break ;
}
2019-07-24 15:58:44 -04:00
2019-07-23 10:35:33 -04:00
if (! await taskreader . ProgressAsync )
2019-07-24 15:58:44 -04:00
{
2019-07-23 10:35:33 -04:00
return ;
2019-07-24 15:58:44 -04:00
}
2016-01-28 22:24:51 +01:00
2019-07-23 10:35:33 -04:00
await self . Output . WriteAsync ( s );
}
2018-04-24 13:11:03 +02:00
}
});
}
/// <summary>
/// A helper class to assist in excluding empty folders
/// </summary>
private class DirectoryStackEntry
{
/// <summary>
/// The path for the folder
/// </summary>
public string Path ;
/// <summary>
/// A flag indicating if any items are found in this folder
/// </summary>
public bool AnyEntries ;
}
/// <summary>
/// Excludes empty folders.
/// </summary>
/// <returns>The list without empty folders.</returns>
/// <param name="source">The list with potential empty folders.</param>
private static IEnumerable < string > ExcludeEmptyFolders ( IEnumerable < string > source )
{
var pathstack = new Stack < DirectoryStackEntry >();
foreach ( var s in source )
{
// Keep track of directories
var isDirectory = s [ s . Length - 1 ] == System . IO . Path . DirectorySeparatorChar ;
if ( isDirectory )
{
2018-05-16 19:40:51 +02:00
while ( pathstack . Count > 0 && ! s . StartsWith ( pathstack . Peek (). Path , Library . Utility . Utility . ClientFilenameStringComparison ))
2018-04-24 13:11:03 +02:00
{
var e = pathstack . Pop ();
if ( e . AnyEntries || pathstack . Count == 0 )
{
// Propagate the any-flag upwards
if ( pathstack . Count > 0 )
pathstack . Peek (). AnyEntries = true ;
yield return e . Path ;
}
else
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingEmptyFolder" , "Excluding empty folder {0}" , e . Path );
}
2018-05-16 19:40:51 +02:00
if ( pathstack . Count == 0 || s . StartsWith ( pathstack . Peek (). Path , Library . Utility . Utility . ClientFilenameStringComparison ))
2018-04-24 13:11:03 +02:00
{
pathstack . Push ( new DirectoryStackEntry () { Path = s });
2016-02-27 02:15:42 +01:00
continue ;
2018-04-24 13:11:03 +02:00
}
}
// Just emit files
else
{
if ( pathstack . Count != 0 )
pathstack . Peek (). AnyEntries = true ;
yield return s ;
}
}
2016-01-28 22:24:51 +01:00
2018-04-24 13:11:03 +02:00
while ( pathstack . Count > 0 )
{
var e = pathstack . Pop ();
if ( e . AnyEntries || pathstack . Count == 0 )
{
// Propagate the any-flag upwards
if ( pathstack . Count > 0 )
pathstack . Peek (). AnyEntries = true ;
yield return e . Path ;
2016-02-27 02:15:42 +01:00
}
2018-04-24 13:11:03 +02:00
}
}
2016-01-28 22:24:51 +01:00
2018-04-24 13:11:03 +02:00
/// <summary>
/// Re-integrates the mixin queue to form a strictly sequential list of results
/// </summary>
/// <returns>The expanded list.</returns>
/// <param name="worklist">The basic enumerable.</param>
/// <param name="mixinqueue">The mix in queue.</param>
/// <param name="emitfilter">The emitfilter.</param>
/// <param name="enumeratefilter">The enumeratefilter.</param>
private static IEnumerable < string > ExpandWorkList ( IEnumerable < string > worklist , Queue < string > mixinqueue , Library . Utility . IFilter emitfilter , Library . Utility . IFilter enumeratefilter )
{
// Process each path, and dequeue the mixins with symlinks as we go
foreach ( var s in worklist )
{
2016-02-27 02:15:42 +01:00
while ( mixinqueue . Count > 0 )
2018-04-24 13:11:03 +02:00
yield return mixinqueue . Dequeue ();
Library . Utility . IFilter m ;
if ( emitfilter != enumeratefilter && ! Library . Utility . FilterExpression . Matches ( emitfilter , s , out m ))
continue ;
yield return s ;
}
// Trailing symlinks are caught here
while ( mixinqueue . Count > 0 )
yield return mixinqueue . Dequeue ();
2016-01-28 22:24:51 +01:00
}
/// <summary>
/// Plugin filter for enumerating a list of files.
/// </summary>
/// <returns>True if the path should be returned, false otherwise.</returns>
/// <param name="path">The current path.</param>
/// <param name="attributes">The file or folder attributes.</param>
2018-10-06 13:30:13 -07:00
private static bool AttributeFilter ( string path , FileAttributes attributes , Snapshots . ISnapshotService snapshot , Library . Utility . IFilter sourcefilter , Options . HardlinkStrategy hardlinkPolicy , Options . SymlinkStrategy symlinkPolicy , Dictionary < string , string > hardlinkmap , FileAttributes fileAttributes , Duplicati . Library . Utility . IFilter enumeratefilter , string [] ignorenames , Queue < string > mixinqueue )
2016-01-28 22:24:51 +01:00
{
2016-08-31 14:17:35 +02:00
// Step 1, exclude block devices
try
2016-01-28 22:24:51 +01:00
{
2016-02-27 02:15:42 +01:00
if ( snapshot . IsBlockDevice ( path ))
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingBlockDevice" , "Excluding block device: {0}" , path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
}
catch ( Exception ex )
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingError" , ex , "Failed to process path: {0}" , path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
// Check if we explicitly include this entry
Duplicati . Library . Utility . IFilter sourcematch ;
bool sourcematches ;
2016-02-27 02:15:42 +01:00
if ( sourcefilter . Matches ( path , out sourcematches , out sourcematch ) && sourcematches )
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "IncludingSourcePath" , "Including source path: {0}" , path );
2018-07-04 13:06:28 -07:00
return true ;
2016-01-28 22:24:51 +01:00
}
// If we have a hardlink strategy, obey it
2016-02-27 02:15:42 +01:00
if ( hardlinkPolicy != Options . HardlinkStrategy . All )
2016-01-28 22:24:51 +01:00
{
try
{
2016-02-27 02:15:42 +01:00
var id = snapshot . HardlinkTargetID ( path );
2016-01-28 22:24:51 +01:00
if ( id != null )
{
2016-02-27 02:15:42 +01:00
if ( hardlinkPolicy == Options . HardlinkStrategy . None )
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingHardlinkByPolicy" , "Excluding hardlink: {0} ({1})" , path , id );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
2016-02-27 02:15:42 +01:00
else if ( hardlinkPolicy == Options . HardlinkStrategy . First )
2016-01-28 22:24:51 +01:00
{
string prevPath ;
2016-02-27 02:15:42 +01:00
if ( hardlinkmap . TryGetValue ( id , out prevPath ))
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingDuplicateHardlink" , "Excluding hardlink ({1}) for: {0}, previous hardlink: {2}" , path , id , prevPath );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
else
{
2016-02-27 02:15:42 +01:00
hardlinkmap . Add ( id , path );
2016-01-28 22:24:51 +01:00
}
}
}
}
catch ( Exception ex )
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingError" , ex , "Failed to process path: {0}" , path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
}
2018-04-24 11:25:37 +02:00
if ( ignorenames != null && ( attributes & FileAttributes . Directory ) != 0 )
{
try
{
foreach ( var n in ignorenames )
{
2018-11-02 21:34:07 +01:00
var ignorepath = SystemIO . IO_OS . PathCombine ( path , n );
2018-04-24 11:25:37 +02:00
if ( snapshot . FileExists ( ignorepath ))
{
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathDueToIgnoreFile" , "Excluding path because ignore file was found: {0}" , ignorepath );
2018-07-04 13:06:28 -07:00
return false ;
2018-04-24 11:25:37 +02:00
}
}
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingError" , ex , "Failed to process path: {0}" , path );
}
}
2016-01-28 22:24:51 +01:00
// If we exclude files based on attributes, filter that
2018-07-04 15:43:37 -07:00
if (( fileAttributes & attributes ) != 0 )
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathFromAttributes" , "Excluding path due to attribute filter: {0}" , path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
// Then check if the filename is not explicitly excluded by a filter
Library . Utility . IFilter match ;
2018-04-12 22:02:31 +02:00
var filtermatch = false ;
2016-02-27 02:15:42 +01:00
if (! Library . Utility . FilterExpression . Matches ( enumeratefilter , path , out match ))
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathFromFilter" , "Excluding path due to filter: {0} => {1}" , path , match == null ? "null" : match . ToString ());
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
else if ( match != null )
{
2018-04-12 22:02:31 +02:00
filtermatch = true ;
2018-04-11 23:02:47 +02:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "IncludingPathFromFilter" , "Including path due to filter: {0} => {1}" , path , match . ToString ());
2016-01-28 22:24:51 +01:00
}
// If the file is a symlink, apply special handling
2018-04-11 23:02:47 +02:00
var isSymlink = snapshot . IsSymlink ( path , attributes );
string symlinkTarget = null ;
if ( isSymlink )
try { symlinkTarget = snapshot . GetSymlinkTarget ( path ); }
catch ( Exception ex ) { Logging . Log . WriteExplicitMessage ( FILTER_LOGTAG , "SymlinkTargetReadError" , ex , "Failed to read symlink target for path: {0}" , path ); }
2016-01-28 22:24:51 +01:00
2018-04-11 23:02:47 +02:00
if ( isSymlink )
2016-01-28 22:24:51 +01:00
{
2018-04-11 23:02:47 +02:00
if (! string . IsNullOrWhiteSpace ( symlinkTarget ))
{
if ( symlinkPolicy == Options . SymlinkStrategy . Ignore )
{
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludeSymlink" , "Excluding symlink: {0}" , path );
2018-07-04 13:06:28 -07:00
return false ;
2018-04-11 23:02:47 +02:00
}
2016-01-28 22:24:51 +01:00
2018-04-11 23:02:47 +02:00
if ( symlinkPolicy == Options . SymlinkStrategy . Store )
{
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "StoreSymlink" , "Storing symlink: {0}" , path );
// We return false because we do not want to recurse into the path,
// but we add the symlink to the mixin so we process the symlink itself
mixinqueue . Enqueue ( path );
2018-07-04 13:06:28 -07:00
return false ;
2018-04-11 23:02:47 +02:00
}
}
else
{
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "FollowingEmptySymlink" , "Treating empty symlink as regular path {0}" , path );
}
2016-01-28 22:24:51 +01:00
}
2018-04-12 22:02:31 +02:00
if (! filtermatch )
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "IncludingPath" , "Including path as no filters matched: {0}" , path );
2016-01-28 22:24:51 +01:00
// All the way through, yes!
2018-07-04 13:06:28 -07:00
return true ;
2016-01-28 22:24:51 +01:00
}
}
}