Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto refresh file list in network shares #8831

Merged
merged 9 commits into from
Apr 8, 2022
3 changes: 2 additions & 1 deletion src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ private async void RecycleBinWatcher_Changed(object sender, FileSystemEventArgs
};
if (e.ChangeType == WatcherChangeTypes.Created)
{
using var folderItem = new ShellItem(e.FullPath);
using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath));
if (folderItem == null) return;
var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem);
response["Item"] = JsonConvert.SerializeObject(shellFileItem);
}
Expand Down
117 changes: 116 additions & 1 deletion src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Vanara.Windows.Shell;
using Windows.ApplicationModel;
using Windows.Foundation.Collections;
Expand All @@ -20,11 +23,18 @@ namespace FilesFullTrust.MessageHandlers
[SupportedOSPlatform("Windows10.0.10240")]
public class Win32MessageHandler : Disposable, IMessageHandler
{
private IList<FileSystemWatcher> dirWatchers;
private PipeStream connection;

public void Initialize(PipeStream connection)
{
this.connection = connection;

DetectIsSetAsDefaultFileManager();
DetectIsSetAsOpenFileDialog();
ApplicationData.Current.LocalSettings.Values["TEMP"] = Environment.GetEnvironmentVariable("TEMP");

dirWatchers = new List<FileSystemWatcher>();
}

private static void DetectIsSetAsDefaultFileManager()
Expand Down Expand Up @@ -159,7 +169,7 @@ public async Task ParseArgumentsAsync(PipeStream connection, Dictionary<string,
return;
}
}

var dataPath = Environment.ExpandEnvironmentVariables("%LocalAppData%\\Files");
if (enable)
{
Expand Down Expand Up @@ -234,6 +244,111 @@ public async Task ParseArgumentsAsync(PipeStream connection, Dictionary<string,
await Win32API.SendMessageAsync(connection, new ValueSet() { { "FileAssociation", await Win32API.GetFileAssociationAsync(filePath, true) } }, message.Get("RequestID", (string)null));
}
break;

case "WatchDirectory":
var watchAction = (string)message["action"];
await ParseWatchDirectoryActionAsync(connection, message, watchAction);
break;
}
}

private async Task ParseWatchDirectoryActionAsync(PipeStream connection, Dictionary<string, object> message, string action)
{
switch (action)
{
case "start":
{
var res = new ValueSet();
var folderPath = (string)message["folderPath"];
if (Directory.Exists(folderPath))
{
var watcher = new FileSystemWatcher
{
Path = folderPath,
Filter = "*.*",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
};
watcher.Created += DirectoryWatcher_Changed;
watcher.Deleted += DirectoryWatcher_Changed;
watcher.Renamed += DirectoryWatcher_Changed;
watcher.EnableRaisingEvents = true;
res.Add("watcherID", watcher.GetHashCode());
dirWatchers.Add(watcher);
}
await Win32API.SendMessageAsync(connection, res, message.Get("RequestID", (string)null));
}
break;

case "cancel":
{
var watcherID = (long)message["watcherID"];
var watcher = dirWatchers.SingleOrDefault(x => x.GetHashCode() == watcherID);
if (watcher != null)
{
dirWatchers.Remove(watcher);
watcher.Dispose();
}
}
break;
}
}

private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Directory watcher event: {e.ChangeType}, {e.FullPath}");
if (connection?.IsConnected ?? false)
{
var response = new ValueSet()
{
{ "FileSystem", Path.GetDirectoryName(e.FullPath) },
{ "Name", e.Name },
{ "Path", e.FullPath },
{ "Type", e.ChangeType.ToString() },
{ "WatcherID", sender.GetHashCode() },
};
if (e.ChangeType == WatcherChangeTypes.Created)
{
var shellFileItem = await GetShellFileItemAsync(e.FullPath);
if (shellFileItem == null) return;
response["Item"] = JsonConvert.SerializeObject(shellFileItem);
}
else if (e.ChangeType == WatcherChangeTypes.Renamed)
{
response["OldPath"] = (e as RenamedEventArgs).OldFullPath;
}
// Send message to UWP app to refresh items
await Win32API.SendMessageAsync(connection, response);
}
}

private async Task<ShellFileItem> GetShellFileItemAsync(string fullPath)
{
while (true)
{
using var hFile = Kernel32.CreateFile(fullPath, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS);
if (!hFile.IsInvalid)
{
using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(fullPath));
if (folderItem == null) return null;
return ShellFolderExtensions.GetShellFileItem(folderItem);
}
var lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
if (lastError != Win32Error.ERROR_SHARING_VIOLATION && lastError != Win32Error.ERROR_LOCK_VIOLATION)
{
return null;
}
await Task.Delay(200);
}
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
foreach (var watcher in dirWatchers)
{
watcher.Dispose();
}
}
}
}
Expand Down
78 changes: 71 additions & 7 deletions src/Files.Uwp/ViewModels/ItemViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public class ItemViewModel : ObservableObject, IDisposable

private IFileListCache fileListCache = FileListCacheController.GetInstance();

public event EventHandler ConnectionChanged;

private NamedPipeAsAppServiceConnection connection;

private NamedPipeAsAppServiceConnection Connection
Expand All @@ -96,6 +98,7 @@ private NamedPipeAsAppServiceConnection Connection
{
connection.RequestReceived += Connection_RequestReceived;
}
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
}

Expand Down Expand Up @@ -431,6 +434,20 @@ private async void Connection_RequestReceived(object sender, Dictionary<string,
}
break;

case "Renamed":
var matchingItem = filesAndFolders.FirstOrDefault(x => x.ItemPath.Equals((string)message["OldPath"], StringComparison.OrdinalIgnoreCase));
if (matchingItem != null)
{
await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(() =>
{
matchingItem.ItemPath = itemPath;
matchingItem.ItemNameRaw = (string)message["Name"];
});
await OrderFilesAndFoldersAsync();
await ApplySingleFileChangeAsync(matchingItem);
}
break;

case "Deleted":
// get the item that immediately follows matching item to be removed
// if the matching item is the last item, try to get the previous item; otherwise, null
Expand Down Expand Up @@ -1287,7 +1304,6 @@ private async Task RapidAddItemsToCollection(string path, LibraryItem library =
currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path));
var syncStatus = await CheckCloudDriveSyncStatusAsync(currentStorageFolder?.Item);
PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown });

WatchForDirectoryChanges(path, syncStatus);
break;

Expand All @@ -1297,7 +1313,11 @@ private async Task RapidAddItemsToCollection(string path, LibraryItem library =
WatchForStorageFolderChanges(currentStorageFolder?.Folder);
break;

case 2: // Do no watch for changes in Box Drive folder to avoid constant refresh (#7428)
case 2: // Watch for changes using FTP in Box Drive folder (#7428) and network drives (#5869)
PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false });
WatchForWin32FolderChanges(path);
break;

case -1: // Enumeration failed
default:
break;
Expand Down Expand Up @@ -1489,9 +1509,10 @@ await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(async () =>
public async Task<int> EnumerateItemsFromStandardFolderAsync(string path, Type sourcePageType, CancellationToken cancellationToken, LibraryItem library = null)
{
// Flag to use FindFirstFileExFromApp or StorageFolder enumeration
var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ?
path.StartsWith(boxFolder) : false;
bool enumFromStorageFolder = isBoxFolder; // Use storage folder for Box Drive (#4629)
var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ?
path.StartsWith(boxFolder) : false; // Use storage folder for Box Drive (#4629)
var isNetworkFolder = System.Text.RegularExpressions.Regex.IsMatch(path, @"^\\\\(?!\?)"); // Use storage folder for network drives (*FromApp methods return access denied)
bool enumFromStorageFolder = isBoxFolder || isNetworkFolder;

BaseStorageFolder rootFolder = null;

Expand Down Expand Up @@ -1582,7 +1603,7 @@ await DialogDisplayHelper.ShowDialogAsync(
}
CurrentFolder = currentFolder;
await EnumFromStorageFolderAsync(path, currentFolder, rootFolder, currentStorageFolder, sourcePageType, cancellationToken);
return isBoxFolder ? 2 : 1; // Workaround for #7428
return isBoxFolder || isNetworkFolder ? 2 : 1; // Workaround for #7428
}
else
{
Expand Down Expand Up @@ -1758,6 +1779,45 @@ await Task.Run(() =>
});
}

private async void WatchForWin32FolderChanges(string folderPath)
{
void RestartWatcher(object sender, EventArgs e)
{
if (Connection != null)
{
ConnectionChanged -= RestartWatcher;
WatchForWin32FolderChanges(folderPath);
}
}
if (Connection != null)
{
var (status, response) = await Connection.SendMessageForResponseAsync(new ValueSet()
{
{ "Arguments", "WatchDirectory" },
{ "action", "start" },
{ "folderPath", folderPath }
});
if (status == AppServiceResponseStatus.Success
&& response.ContainsKey("watcherID"))
{
ConnectionChanged += RestartWatcher;
watcherCTS.Token.Register(async () =>
{
ConnectionChanged -= RestartWatcher;
if (Connection != null)
{
await Connection.SendMessageAsync(new ValueSet()
{
{ "Arguments", "WatchDirectory" },
{ "action", "cancel" },
{ "watcherID", (long)response["watcherID"] }
});
}
});
}
}
}

private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args)
{
//query options have to be reapplied otherwise old results are returned
Expand Down Expand Up @@ -2122,7 +2182,11 @@ private async Task AddFileOrFolderAsync(ListedItem item)
return;
}

filesAndFolders.Add(item);
if (!filesAndFolders.Any(x => x.ItemPath.Equals(item.ItemPath, StringComparison.OrdinalIgnoreCase))) // Avoid adding duplicate items
{
filesAndFolders.Add(item);
}

enumFolderSemaphore.Release();
}

Expand Down