Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Files.App/Data/Items/ListedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ public BitmapImage ShieldIcon
public string ItemPath
{
get => itemPath;
set => SetProperty(ref itemPath, value);
set
{
if (SetProperty(ref itemPath, value))
OnPropertyChanged(nameof(Name));
}
}

private string itemNameRaw;
Expand Down
3 changes: 3 additions & 0 deletions src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public static partial class Win32PInvoke
public const int FILE_NOTIFY_CHANGE_LAST_ACCESS = 32;
public const int FILE_NOTIFY_CHANGE_CREATION = 64;
public const int FILE_NOTIFY_CHANGE_SECURITY = 256;
public const int FILE_NOTIFY_CHANGE_STREAM_NAME = 512;
public const int FILE_NOTIFY_CHANGE_STREAM_SIZE = 1024;
public const int FILE_NOTIFY_CHANGE_STREAM_WRITE = 2048;

public const int INVALID_HANDLE_VALUE = -1;
public const int FILE_SHARE_READ = 0x00000001;
Expand Down
102 changes: 101 additions & 1 deletion src/Files.App/ViewModels/ShellViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,9 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat
if (hasSyncStatus)
notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES;

if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible)
notifyFilters |= FILE_NOTIFY_CHANGE_STREAM_NAME | FILE_NOTIFY_CHANGE_STREAM_SIZE | FILE_NOTIFY_CHANGE_STREAM_WRITE;

var overlapped = new OVERLAPPED();
using var eventHandle = PInvoke.CreateEvent(null, false, false, null);
overlapped.hEvent = eventHandle.DangerousGetHandle();
Expand Down Expand Up @@ -2593,6 +2596,12 @@ private async Task ProcessOperationQueueAsync(CancellationToken cancellationToke
const uint FILE_ACTION_MODIFIED = 0x00000003;
const uint FILE_ACTION_RENAMED_OLD_NAME = 0x00000004;
const uint FILE_ACTION_RENAMED_NEW_NAME = 0x00000005;
const uint FILE_ACTION_ADDED_STREAM = 0x00000006;
const uint FILE_ACTION_REMOVED_STREAM = 0x00000007;
const uint FILE_ACTION_MODIFIED_STREAM = 0x00000008;

// Not a system action; requeued by the stream cases below
const uint FILE_ACTION_RECHECK_STREAM = 0xFFFFFFFF;

const int UPDATE_BATCH_SIZE = 32;
var sampler = new IntervalSampler(200);
Expand Down Expand Up @@ -2644,6 +2653,11 @@ async Task HandleChangesOccurredAsync()
case FILE_ACTION_MODIFIED:
if (!updateQueue.Contains(operation.FileName))
updateQueue.Enqueue(operation.FileName);

// Some filesystems report stream deletion only as a modification
// of the host file, without any FILE_ACTION_*_STREAM action
if (await SyncAlternateStreamsForFileAsync(operation.FileName))
anyEdits = true;
break;

case FILE_ACTION_REMOVED:
Expand All @@ -2662,13 +2676,20 @@ async Task HandleChangesOccurredAsync()
{
operationQueue.TryDequeue(out _);
var newPath = nextOp.FileName;
var oldPath = operation.FileName;
await dispatcherQueue.EnqueueOrInvokeAsync(() =>
{
renamed.ItemPath = newPath;
renamed.ItemNameRaw = Path.GetFileName(newPath);
if (renamed.PrimaryItemAttribute == StorageItemTypes.File)
renamed.FileExtension = Path.GetExtension(newPath);

foreach (var adsItem in filesAndFolders.ToList().OfType<AlternateStreamItem>().Where(x => x.MainStreamPath.Equals(oldPath, StringComparison.OrdinalIgnoreCase)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var adsItem in filesAndFolders.ToList().OfType<AlternateStreamItem>().Where(x => x.MainStreamPath.Equals(oldPath, StringComparison.OrdinalIgnoreCase)))
foreach (var adsItem in filesAndFolders.OfType<AlternateStreamItem>().Where(x => x.MainStreamPath.Equals(oldPath, StringComparison.OrdinalIgnoreCase)))

adsItem.ItemPath = $"{newPath}:{adsItem.ItemNameRaw}";
});

if (await SyncAlternateStreamsForFileAsync(newPath))
anyEdits = true;
}
else
{
Expand All @@ -2677,6 +2698,24 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
anyEdits = true;
}
break;

case FILE_ACTION_ADDED_STREAM:
case FILE_ACTION_REMOVED_STREAM:
case FILE_ACTION_MODIFIED_STREAM:
case FILE_ACTION_RECHECK_STREAM:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(You can add curly braces {} for cases)

if (await SyncAlternateStreamsAsync(operation.FileName))
anyEdits = true;
else if (operation.Action != FILE_ACTION_RECHECK_STREAM)
{
// The notification can arrive before the operation is visible on disk
var streamPath = operation.FileName;
_ = Task.Delay(500).ContinueWith(_ =>
{
operationQueue.Enqueue((FILE_ACTION_RECHECK_STREAM, streamPath));
operationEvent.Set();
});
}
break;
}
}
catch (Exception ex)
Expand Down Expand Up @@ -2945,7 +2984,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible)
{
// Main file is removed, remove connected ADS
foreach (var adsItem in filesAndFolders.ToList().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath == matchingItem.ItemPath))
foreach (var adsItem in filesAndFolders.ToList().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath.Equals(matchingItem.ItemPath, StringComparison.OrdinalIgnoreCase)))
Comment on lines -2948 to +2987

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var adsItem in filesAndFolders.ToList().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath == matchingItem.ItemPath))
foreach (var adsItem in filesAndFolders.ToList().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath.Equals(matchingItem.ItemPath, StringComparison.OrdinalIgnoreCase)))
foreach (var adsItem in filesAndFolders.ToArray().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath.Equals(matchingItem.ItemPath, StringComparison.OrdinalIgnoreCase)))

filesAndFolders.Remove(adsItem);
}

Expand All @@ -2960,6 +2999,67 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
return null;
}

private Task<bool> SyncAlternateStreamsAsync(string streamPath)
{
// Stream notifications name the stream as "<file>:<stream>"; a colon not past the
// last separator is the drive colon, i.e. an event for the unnamed data stream
var separatorIndex = streamPath.LastIndexOf(':');
if (separatorIndex <= streamPath.LastIndexOf('\\'))
return Task.FromResult(false);

return SyncAlternateStreamsForFileAsync(streamPath.Substring(0, separatorIndex));
}

private async Task<bool> SyncAlternateStreamsForFileAsync(string mainStreamPath)
{
if (!UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible)
return false;

try
{
await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token);
}
catch (OperationCanceledException)
{
return false;
}

try
{
var items = filesAndFolders.ToList();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var items = filesAndFolders.ToList();
var items = filesAndFolders.ToArray();

var mainItem = items.FirstOrDefault(x => x is not AlternateStreamItem && x.ItemPath.Equals(mainStreamPath, StringComparison.OrdinalIgnoreCase));
if (mainItem is null)
return false;

var onDisk = Win32Helper.GetAlternateStreams(mainStreamPath)
.Select(ads => Win32StorageEnumerator.GetAlternateStream(ads, mainItem))
.ToDictionary(x => x.ItemPath, StringComparer.OrdinalIgnoreCase);

var anyChanges = false;

foreach (var adsItem in items.OfType<AlternateStreamItem>().Where(x => x.MainStreamPath.Equals(mainStreamPath, StringComparison.OrdinalIgnoreCase)))
{
if (!onDisk.Remove(adsItem.ItemPath))
{
filesAndFolders.Remove(adsItem);
anyChanges = true;
}
}

foreach (var adsItem in onDisk.Values)
{
filesAndFolders.Add(adsItem);
anyChanges = true;
}

return anyChanges;
}
finally
{
enumFolderSemaphore.Release();
}
}

public async Task AddSearchResultsToCollectionAsync(ObservableCollection<ListedItem> searchItems, string currentSearchPath)
{
filesAndFolders.Clear();
Expand Down
Loading