2025-01-14 14:03:48 +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-07-30 09:06:54 +02:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
2025-02-17 14:01:08 +01:00
#nullable enable
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 ;
2025-01-14 14:03:48 +01:00
using Duplicati.Library.Interface ;
2025-02-09 16:14:22 +01:00
using System.Runtime.CompilerServices ;
using Duplicati.Library.SourceProvider ;
2025-02-19 14:00:14 +01:00
using Duplicati.Library.Snapshots.USN ;
2019-07-23 10:35:33 -04:00
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 ));
2024-12-18 08:27:59 +01:00
public static Task Run (
Channels channels ,
2025-02-09 16:14:22 +01:00
ISourceProvider sourceProvider ,
2025-02-17 14:01:08 +01:00
UsnJournalService ? journalService ,
2025-02-09 16:14:22 +01:00
FileAttributes fileAttributeFilter ,
2024-12-18 08:27:59 +01:00
Library . Utility . IFilter emitfilter ,
Options . SymlinkStrategy symlinkPolicy ,
Options . HardlinkStrategy hardlinkPolicy ,
bool excludeemptyfolders ,
2025-02-17 14:01:08 +01:00
string []? ignorenames ,
2024-12-18 08:27:59 +01:00
HashSet < string > blacklistPaths ,
2025-02-17 14:01:08 +01:00
IEnumerable < string >? changedfilelist ,
2024-12-18 08:27:59 +01:00
ITaskReader taskreader ,
2025-02-17 14:01:08 +01:00
Action ? onStopRequested ,
2024-12-18 08:27:59 +01:00
CancellationToken token )
2016-02-27 02:15:42 +01:00
{
return AutomationExtensions . RunTask (
2018-04-24 13:11:03 +02:00
new
{
2024-12-09 13:55:22 +01:00
Output = channels . SourcePaths . AsWrite ()
2016-02-27 02:15:42 +01:00
},
async self =>
{
2019-07-23 10:35:33 -04:00
if (! token . IsCancellationRequested )
{
2025-02-09 16:14:22 +01:00
// The hardlink map tracks the hardlink targets we have seen
// and avoid multiple processing of the same contents
2019-07-23 10:35:33 -04:00
var hardlinkmap = new Dictionary < string , string >();
2025-02-09 16:14:22 +01:00
// The mixin queue is used to store symlinks that should be processed
// The symlinks are emitted during the enumeration process when they are found
2025-02-21 16:06:55 +01:00
var mixinqueue = new Queue < ISourceProviderEntry >();
2025-02-09 16:14:22 +01:00
// The enumeration filter is used to determine what paths to
// recurse into. If the emit filter only has includes,
// the enumeration filter will also include all folders,
// as nothing will match otherwise
var enumeratefilter = emitfilter ;
2016-02-27 02:15:42 +01:00
2024-10-28 09:00:40 +01:00
Library . Utility . FilterExpression . AnalyzeFilters ( emitfilter , out var includes , out var excludes );
2019-07-23 10:35:33 -04:00
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-24 15:58:44 -04:00
2025-02-18 22:12:37 +01:00
// Shared filter function with bound variables
2025-02-21 16:06:55 +01:00
ValueTask < bool > FilterEntry ( ISourceProviderEntry entry )
2025-02-18 22:12:37 +01:00
=> SourceFileEntryFilter ( entry , blacklistPaths , hardlinkPolicy , symlinkPolicy , hardlinkmap , fileAttributeFilter , enumeratefilter , ignorenames , mixinqueue , token );
// Prepare the work list
2025-02-21 16:06:55 +01:00
IAsyncEnumerable < ISourceProviderEntry > worklist ;
2025-02-18 22:12:37 +01:00
// If we have a specific list, use that instead of enumerating the filesystem
2025-02-09 16:14:22 +01:00
if ( changedfilelist != null && changedfilelist . Any ())
{
2025-02-21 16:06:55 +01:00
async IAsyncEnumerable < ISourceProviderEntry > ExpandSources ( IEnumerable < string > list )
2025-02-18 22:12:37 +01:00
{
foreach ( var s in list )
{
var r = await sourceProvider . GetEntry ( s , s . EndsWith ( Path . DirectorySeparatorChar ), token ). ConfigureAwait ( false );
if ( r != null )
{
//TODO: Set r.IsRoot = true for source elements
yield return r ;
}
}
}
worklist = ExpandSources ( changedfilelist ). WhereAwait ( FilterEntry );
2025-02-09 16:14:22 +01:00
}
else if ( journalService != null )
{
if (! OperatingSystem . IsWindows ())
throw new NotSupportedException ( "USN is only supported on Windows" );
var fileProviders = ( sourceProvider is Combiner c ? c . Providers . AsEnumerable () : [ sourceProvider ])
2025-02-17 14:01:08 +01:00
. OfType < SourceProvider . LocalFileSource >()
2025-02-09 16:14:22 +01:00
. ToList ();
if ( fileProviders . Count <= 0 )
throw new InvalidOperationException ( "No file providers found, but USN was enabled?" );
if ( fileProviders . Count > 1 )
throw new InvalidOperationException ( "Multiple file providers found, but USN only supports one" );
2025-02-18 22:12:37 +01:00
// TODO: This is not as effecient as possible.
// If the root folder is marked changed by USN, the expansion with RecurseEntries
// will cause a full regular scan. It should be possible to *only* process the
// changed elements as returned from the USN journal.
// It should be possible to remove RecurseEntries from the GetModifiedSources()
// enumeration result.
// Such a change requires significant testing as there are many pitfalls with USN.
worklist = RecurseEntries ( journalService . GetModifiedSources ( FilterEntry , token ),
FilterEntry ,
2025-02-09 16:14:22 +01:00
token
2025-02-18 22:12:37 +01:00
)
. Concat (
RecurseEntries ( journalService . GetFullScanSources ( token ),
FilterEntry ,
token )
2025-02-09 16:14:22 +01:00
);
2019-07-23 10:35:33 -04:00
}
else
{
2025-02-09 16:14:22 +01:00
worklist = RecurseEntries ( sourceProvider . Enumerate ( token ),
2025-02-18 22:12:37 +01:00
FilterEntry ,
2025-02-09 16:14:22 +01:00
token
);
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
2025-02-09 16:14:22 +01:00
var source = ExpandWorkList ( worklist , mixinqueue , emitfilter , enumeratefilter , token );
// TODO: There was a call to DistinctBy here, but this would cause all paths to be stored in memory
//.DistinctBy(x => x.Path, Library.Utility.Utility.IsFSCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
2019-07-23 10:35:33 -04:00
if ( excludeemptyfolders )
2025-02-09 16:14:22 +01:00
source = ExcludeEmptyFolders ( source , token );
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
2025-02-09 16:14:22 +01:00
await foreach ( var s in source . WithCancellation ( token ). ConfigureAwait ( false ))
2019-07-23 10:35:33 -04:00
{
2024-12-18 08:27:59 +01:00
#if DEBUG
// For testing purposes, we need exact control
// when requesting a process stop.
// The "onStopRequested" callback is used to detect
// if the process is the real file enumeration process
// because the counter processe does not have a callback
if ( onStopRequested != null )
2025-02-09 16:14:22 +01:00
taskreader . TestMethodCallback ?. Invoke ( s . Path );
2024-12-18 08:27:59 +01:00
#endif
// Stop if requested
if ( token . IsCancellationRequested || ! await taskreader . ProgressRendevouz (). ConfigureAwait ( false ))
2019-07-24 15:58:44 -04:00
{
2024-12-18 08:27:59 +01:00
onStopRequested ?. Invoke ();
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>
2025-02-09 16:14:22 +01:00
/// The item being tracked
2018-04-24 13:11:03 +02:00
/// </summary>
2025-02-21 16:06:55 +01:00
public required ISourceProviderEntry Item ;
2018-04-24 13:11:03 +02:00
/// <summary>
/// A flag indicating if any items are found in this folder
/// </summary>
2025-02-17 14:01:08 +01:00
public required bool AnyEntries ;
2018-04-24 13:11:03 +02:00
}
/// <summary>
/// Excludes empty folders.
/// </summary>
/// <returns>The list without empty folders.</returns>
/// <param name="source">The list with potential empty folders.</param>
2025-02-21 16:06:55 +01:00
private static async IAsyncEnumerable < ISourceProviderEntry > ExcludeEmptyFolders ( IAsyncEnumerable < ISourceProviderEntry > source , [ EnumeratorCancellation ] CancellationToken cancellationToken )
2018-04-24 13:11:03 +02:00
{
var pathstack = new Stack < DirectoryStackEntry >();
2025-02-09 16:14:22 +01:00
await foreach ( var s in source . WithCancellation ( cancellationToken ). ConfigureAwait ( false ))
2018-04-24 13:11:03 +02:00
{
// Keep track of directories
2025-02-09 16:14:22 +01:00
var isDirectory = s . Path [ s . Path . Length - 1 ] == System . IO . Path . DirectorySeparatorChar ;
2018-04-24 13:11:03 +02:00
if ( isDirectory )
{
2025-02-09 16:14:22 +01:00
while ( pathstack . Count > 0 && ! s . Path . StartsWith ( pathstack . Peek (). Item . 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 ;
2024-07-30 09:06:54 +02:00
2025-02-09 16:14:22 +01:00
yield return e . Item ;
2018-04-24 13:11:03 +02:00
}
else
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingEmptyFolder" , "Excluding empty folder {0}" , e . Item );
2018-04-24 13:11:03 +02:00
}
2025-02-09 16:14:22 +01:00
if ( pathstack . Count == 0 || s . Path . StartsWith ( pathstack . Peek (). Item . Path , Library . Utility . Utility . ClientFilenameStringComparison ))
2018-04-24 13:11:03 +02:00
{
2025-02-17 14:01:08 +01:00
pathstack . Push ( new DirectoryStackEntry () { Item = s , AnyEntries = false });
2016-02-27 02:15:42 +01:00
continue ;
2018-04-24 13:11:03 +02:00
}
}
// Just emit files
2024-07-30 09:06:54 +02:00
else
2018-04-24 13:11:03 +02:00
{
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 ();
2024-07-30 09:06:54 +02:00
if ( e . AnyEntries || pathstack . Count == 0 )
2018-04-24 13:11:03 +02:00
{
// Propagate the any-flag upwards
if ( pathstack . Count > 0 )
pathstack . Peek (). AnyEntries = true ;
2025-02-09 16:14:22 +01:00
yield return e . Item ;
}
}
}
/// <summary>
/// Performs recursive traversal of the sources
/// </summary>
/// <param name="entries">The entries to recurse</param>
/// <param name="filter">The filter to apply</param>
/// <returns></returns>
2025-02-21 16:06:55 +01:00
private static async IAsyncEnumerable < ISourceProviderEntry > RecurseEntries ( IAsyncEnumerable < ISourceProviderEntry > entries , Func < ISourceProviderEntry , ValueTask < bool >> filter , [ EnumeratorCancellation ] CancellationToken cancellationToken )
2025-02-09 16:14:22 +01:00
{
2025-02-21 16:06:55 +01:00
var work = new Stack < ISourceProviderEntry >();
2025-02-09 16:14:22 +01:00
await foreach ( var e in entries . WithCancellation ( cancellationToken ). ConfigureAwait ( false ))
if ( await filter ( e ). ConfigureAwait ( false ))
work . Push ( e );
while ( work . Count > 0 )
{
var e = work . Pop ();
// Process meta entry contents, but don't emit them for processing
if (! e . IsMetaEntry )
yield return e ;
if ( e . IsFolder )
{
try
{
// We only filter new items, as we assume the input is already filtered
await foreach ( var r in e . Enumerate ( cancellationToken ). ConfigureAwait ( false ))
if ( await filter ( r ). ConfigureAwait ( false ))
work . Push ( r );
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorEnumerate" , ex , "Failed to enumerate path: {0}" , 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>
2025-02-21 16:06:55 +01:00
private static async IAsyncEnumerable < ISourceProviderEntry > ExpandWorkList ( IAsyncEnumerable < ISourceProviderEntry > worklist , Queue < ISourceProviderEntry > mixinqueue , Library . Utility . IFilter emitfilter , Library . Utility . IFilter enumeratefilter , [ EnumeratorCancellation ] CancellationToken cancellationToken )
2018-04-24 13:11:03 +02:00
{
// Process each path, and dequeue the mixins with symlinks as we go
2025-02-09 16:14:22 +01:00
await foreach ( var s in worklist . WithCancellation ( cancellationToken ). ConfigureAwait ( false ))
2018-04-24 13:11:03 +02:00
{
2016-02-27 02:15:42 +01:00
while ( mixinqueue . Count > 0 )
2018-04-24 13:11:03 +02:00
yield return mixinqueue . Dequeue ();
2025-02-09 16:14:22 +01:00
// If there are only includes in the filter, check if the item is in the original filter
// Since the enumerate filter also includes all folders, we need to ensure we do not emit
// any entries that are filtered explicitly by the user
if ( emitfilter != enumeratefilter && ! Library . Utility . FilterExpression . Matches ( emitfilter , s . Path , out var _ ))
2018-04-24 13:11:03 +02:00
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>
2025-02-09 16:14:22 +01:00
/// Performs a pre-filter on the source entry to see if it should be included in the backup
2016-01-28 22:24:51 +01:00
/// </summary>
2025-02-09 16:14:22 +01:00
/// <param name="entry">The entry to evaluate.</param>
2024-10-28 09:00:40 +01:00
/// <param name="blacklistPaths">The blacklist paths.</param>
2025-02-09 16:14:22 +01:00
/// <returns>True if the path should be returned, false otherwise.</returns>
2025-02-21 16:06:55 +01:00
private static bool PreFilterSourceEntry ( ISourceProviderEntry entry , HashSet < string > blacklistPaths )
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
// Don't filter meta stuff
if ( entry . IsMetaEntry )
return true ;
2024-10-28 09:00:40 +01:00
// Exclude any blacklisted paths
2025-02-09 16:14:22 +01:00
if ( blacklistPaths . Contains ( entry . Path ))
2024-10-28 09:00:40 +01:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingBlacklistedPath" , "Excluding blacklisted path: {0}" , entry . Path );
2024-10-28 09:00:40 +01:00
return false ;
}
// Exclude block devices
2024-07-30 09:06:54 +02:00
try
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
if ( entry . IsBlockDevice )
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingBlockDevice" , "Excluding block device: {0}" , entry . Path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
}
catch ( Exception ex )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorBlockDevice" , ex , "Failed to process path: {0}" , entry . Path );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
2025-02-09 16:14:22 +01:00
// Exclude character devices
try
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
if ( entry . IsCharacterDevice )
{
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingCharacterDevice" , "Excluding character device: {0}" , entry . Path );
return false ;
}
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorCharacterDevice" , ex , "Failed to process path: {0}" , entry . Path );
return false ;
2016-01-28 22:24:51 +01:00
}
2025-02-09 16:14:22 +01:00
return true ;
}
/// <summary>
/// Evaluates a single entry for inclusion in the backup
/// </summary>
/// <param name="entry">The current entry.</param>
/// <param name="snapshot">The snapshot service.</param>
/// <param name="blacklistPaths">The blacklist paths.</param>
/// <param name="hardlinkPolicy">The hardlink policy.</param>
/// <param name="symlinkPolicy">The symlink policy.</param>
/// <param name="hardlinkmap">The hardlink map.</param>
/// <param name="fileAttributeFilter">The file attributes to exclude.</param>
/// <param name="enumeratefilter">The enumerate filter.</param>
/// <param name="ignorenames">The ignore names.</param>
/// <param name="mixinqueue">The mixin queue.</param>
/// <returns>True if the path should be returned, false otherwise.</returns>
2025-02-21 16:06:55 +01:00
private static async ValueTask < bool > SourceFileEntryFilter ( ISourceProviderEntry entry , HashSet < string > blacklistPaths , Options . HardlinkStrategy hardlinkPolicy , Options . SymlinkStrategy symlinkPolicy , Dictionary < string , string > hardlinkmap , FileAttributes fileAttributeFilter , Duplicati . Library . Utility . IFilter enumeratefilter , string []? ignorenames , Queue < ISourceProviderEntry > mixinqueue , CancellationToken cancellationToken )
2025-02-09 16:14:22 +01:00
{
// Do the course pre-filtering first
if (! PreFilterSourceEntry ( entry , blacklistPaths ))
return false ;
// Never exclude the root entries
if ( entry . IsRootEntry )
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
{
2025-02-09 16:14:22 +01:00
var id = entry . HardlinkTargetId ;
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
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingHardlinkByPolicy" , "Excluding hardlink: {0} ({1})" , entry . 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
{
2025-02-17 14:01:08 +01:00
if ( hardlinkmap . TryGetValue ( id , out var prevPath ))
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingDuplicateHardlink" , "Excluding hardlink ({1}) for: {0}, previous hardlink: {2}" , entry . Path , id , prevPath );
2018-07-04 13:06:28 -07:00
return false ;
2016-01-28 22:24:51 +01:00
}
else
{
2025-02-09 16:14:22 +01:00
hardlinkmap . Add ( id , entry . Path );
2016-01-28 22:24:51 +01:00
}
}
}
}
catch ( Exception ex )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorHardLink" , ex , "Failed to process path: {0}" , entry . Path );
2018-07-04 13:06:28 -07:00
return false ;
2024-07-30 09:06:54 +02:00
}
2016-01-28 22:24:51 +01:00
}
2025-02-09 16:14:22 +01:00
// Check if there is an ignore marker file
if ( ignorenames != null && entry . IsFolder )
2018-04-24 11:25:37 +02:00
{
try
{
foreach ( var n in ignorenames )
{
2025-02-09 16:14:22 +01:00
if ( await entry . FileExists ( n , cancellationToken ). ConfigureAwait ( false ))
2018-04-24 11:25:37 +02:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathDueToIgnoreFile" , "Excluding path because ignore file {0} was found in: {1}" , n , entry . Path );
2018-07-04 13:06:28 -07:00
return false ;
2018-04-24 11:25:37 +02:00
}
}
}
2024-07-30 09:06:54 +02:00
catch ( Exception ex )
2018-04-24 11:25:37 +02:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorIgnoreFile" , ex , "Failed to process path: {0}" , entry . Path );
2018-04-24 11:25:37 +02:00
}
}
2025-02-09 16:14:22 +01:00
// Setup some basic processing attributes
var attributes = entry . IsFolder
? FileAttributes . Directory
: FileAttributes . Normal ;
try
{
attributes = entry . Attributes ;
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( FILTER_LOGTAG , "PathProcessingErrorAttributes" , ex , "Failed to process path, using default attributes: {0}" , entry . Path );
}
2016-01-28 22:24:51 +01:00
// If we exclude files based on attributes, filter that
2025-02-09 16:14:22 +01:00
if (( fileAttributeFilter & attributes ) != 0 )
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathFromAttributes" , "Excluding path due to attribute filter: {0}" , entry . 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
2018-04-12 22:02:31 +02:00
var filtermatch = false ;
2025-02-09 16:14:22 +01:00
if (! Library . Utility . FilterExpression . Matches ( enumeratefilter , entry . Path , out var match ))
2016-01-28 22:24:51 +01:00
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludingPathFromFilter" , "Excluding path due to filter: {0} => {1}" , entry . 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 ;
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "IncludingPathFromFilter" , "Including path due to filter: {0} => {1}" , entry . Path , match . ToString ());
2016-01-28 22:24:51 +01:00
}
// If the file is a symlink, apply special handling
2025-02-17 14:01:08 +01:00
string? symlinkTarget = null ;
2025-02-09 16:14:22 +01:00
try
{
symlinkTarget = entry . SymlinkTarget ;
}
catch ( Exception ex )
{
Logging . Log . WriteExplicitMessage ( FILTER_LOGTAG , "SymlinkTargetReadError" , ex , "Failed to read symlink target for path: {0}" , entry . Path );
}
2016-01-28 22:24:51 +01:00
2025-02-09 16:14:22 +01:00
if ( symlinkTarget != null )
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 )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "ExcludeSymlink" , "Excluding symlink: {0}" , entry . 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 )
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "StoreSymlink" , "Storing symlink: {0}" , entry . Path );
2018-04-11 23:02:47 +02:00
// 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
2025-02-09 16:14:22 +01:00
mixinqueue . Enqueue ( entry );
2018-07-04 13:06:28 -07:00
return false ;
2018-04-11 23:02:47 +02:00
}
}
else
{
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "FollowingEmptySymlink" , "Treating empty symlink as regular path {0}" , entry . Path );
2018-04-11 23:02:47 +02:00
}
2016-01-28 22:24:51 +01:00
}
2018-04-12 22:02:31 +02:00
if (! filtermatch )
2025-02-09 16:14:22 +01:00
Logging . Log . WriteVerboseMessage ( FILTER_LOGTAG , "IncludingPath" , "Including path as no filters matched: {0}" , entry . Path );
2018-04-12 22:02:31 +02:00
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
}
}
}