mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-11-23 19:49:17 +08:00
[Peek] Improve folder enumeration (#35076)
* Improve folder enumeration. Fix initial display of folder details. Simplify UnsupportedFilePreviewer. Use Progress method for reporting progress. #35008 * Remove unused using. * Apply suggestions from code review Co-authored-by: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com> * Implement PR review suggestions, including fix for InaccessibleFiles not being skipped. * Folder enumeration avoids Reparse Points. Moved enumeration options to field. * Remove unused using. * Make folder enumeration options static...again.
This commit is contained in:
parent
e99b52fb35
commit
8fb45b5a55
@ -5,13 +5,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
using Peek.Common.Extensions;
|
using Peek.Common.Extensions;
|
||||||
using Peek.Common.Helpers;
|
using Peek.Common.Helpers;
|
||||||
@ -22,27 +22,42 @@ using Windows.Foundation;
|
|||||||
|
|
||||||
namespace Peek.FilePreviewer.Previewers
|
namespace Peek.FilePreviewer.Previewers
|
||||||
{
|
{
|
||||||
public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable
|
public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer
|
||||||
{
|
{
|
||||||
private static readonly EnumerationOptions _fileEnumOptions = new() { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = true };
|
/// <summary>
|
||||||
private static readonly EnumerationOptions _directoryEnumOptions = new() { MatchType = MatchType.Win32, AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = true };
|
/// The number of files to scan between updates when calculating folder size.
|
||||||
private readonly DispatcherTimer _folderSizeDispatcherTimer = new();
|
/// </summary>
|
||||||
private ulong _folderSize;
|
private const int FolderEnumerationChunkSize = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum view updates per second when enumerating a folder's contents.
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxUpdateFps = 15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The icon to display when a file or folder's thumbnail or icon could not be retrieved.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly SvgImageSource DefaultIcon = new(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The options to use for the folder size enumeration. We recurse through all files and all subfolders.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly EnumerationOptions FolderEnumerationOptions;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private UnsupportedFilePreviewData preview = new UnsupportedFilePreviewData();
|
private UnsupportedFilePreviewData preview = new();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private PreviewState state;
|
private PreviewState state;
|
||||||
|
|
||||||
|
static UnsupportedFilePreviewer()
|
||||||
|
{
|
||||||
|
FolderEnumerationOptions = new() { RecurseSubdirectories = true, AttributesToSkip = FileAttributes.ReparsePoint };
|
||||||
|
}
|
||||||
|
|
||||||
public UnsupportedFilePreviewer(IFileSystemItem file)
|
public UnsupportedFilePreviewer(IFileSystemItem file)
|
||||||
{
|
{
|
||||||
_folderSizeDispatcherTimer.Interval = TimeSpan.FromMilliseconds(500);
|
|
||||||
_folderSizeDispatcherTimer.Tick += FolderSizeDispatcherTimer_Tick;
|
|
||||||
|
|
||||||
Item = file;
|
Item = file;
|
||||||
Preview.FileName = file.Name;
|
|
||||||
Preview.DateModified = file.DateModified?.ToString(CultureInfo.CurrentCulture);
|
|
||||||
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,41 +65,40 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
|
|
||||||
private DispatcherQueue Dispatcher { get; }
|
private DispatcherQueue Dispatcher { get; }
|
||||||
|
|
||||||
private Task<bool>? IconPreviewTask { get; set; }
|
public Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken) =>
|
||||||
|
Task.FromResult(new PreviewSize { MonitorSize = new Size(680, 500), UseEffectivePixels = true });
|
||||||
private Task<bool>? DisplayInfoTask { get; set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_folderSizeDispatcherTimer.Tick -= FolderSizeDispatcherTimer_Tick;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Size? size = new Size(680, 500);
|
|
||||||
var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true };
|
|
||||||
return Task.FromResult(previewSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
try
|
||||||
|
|
||||||
State = PreviewState.Loading;
|
|
||||||
|
|
||||||
IconPreviewTask = LoadIconPreviewAsync(cancellationToken);
|
|
||||||
DisplayInfoTask = LoadDisplayInfoAsync(cancellationToken);
|
|
||||||
|
|
||||||
await Task.WhenAll(IconPreviewTask, DisplayInfoTask);
|
|
||||||
|
|
||||||
if (HasFailedLoadingPreview())
|
|
||||||
{
|
{
|
||||||
State = PreviewState.Error;
|
await Dispatcher.RunOnUiThread(async () =>
|
||||||
|
{
|
||||||
|
Preview.FileName = Item.Name;
|
||||||
|
Preview.DateModified = Item.DateModified?.ToString(CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
State = PreviewState.Loaded;
|
||||||
|
|
||||||
|
await LoadIconPreviewAsync(cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var progress = new Progress<string>(update =>
|
||||||
|
{
|
||||||
|
Dispatcher.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
Preview.FileSize = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await LoadDisplayInfoAsync(progress, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
State = PreviewState.Loaded;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("UnsupportedFilePreviewer error.", ex);
|
||||||
|
State = PreviewState.Error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,139 +111,59 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LoadIconPreviewAsync(CancellationToken cancellationToken)
|
private async Task LoadIconPreviewAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
bool isIconValid = false;
|
Preview.IconPreview = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) ??
|
||||||
|
await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken) ??
|
||||||
var isTaskSuccessful = await TaskExtension.RunSafe(async () =>
|
DefaultIcon;
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Dispatcher.RunOnUiThread(async () =>
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var iconBitmap = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken)
|
|
||||||
?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
isIconValid = iconBitmap != null;
|
|
||||||
|
|
||||||
Preview.IconPreview = iconBitmap ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return isIconValid && isTaskSuccessful;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
|
private async Task LoadDisplayInfoAsync(IProgress<string> sizeProgress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
bool isDisplayValid = false;
|
string type = await Item.GetContentTypeAsync();
|
||||||
|
|
||||||
var isTaskSuccessful = await TaskExtension.RunSafe(async () =>
|
Dispatcher.TryEnqueue(() => Preview.FileType = type);
|
||||||
|
|
||||||
|
if (Item is FolderItem folderItem)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
await Task.Run(() => CalculateFolderSizeWithProgress(Item.Path, sizeProgress, cancellationToken), cancellationToken);
|
||||||
|
|
||||||
var type = await Task.Run(Item.GetContentTypeAsync);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
isDisplayValid = type != null;
|
|
||||||
|
|
||||||
var readableFileSize = string.Empty;
|
|
||||||
|
|
||||||
if (Item is FileItem)
|
|
||||||
{
|
|
||||||
readableFileSize = ReadableStringHelper.BytesToReadableString(Item.FileSizeBytes);
|
|
||||||
}
|
|
||||||
else if (Item is FolderItem)
|
|
||||||
{
|
|
||||||
ComputeFolderSize(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Dispatcher.RunOnUiThread(() =>
|
|
||||||
{
|
|
||||||
Preview.FileSize = readableFileSize;
|
|
||||||
Preview.FileType = type;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return isDisplayValid && isTaskSuccessful;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasFailedLoadingPreview()
|
|
||||||
{
|
|
||||||
var isLoadingIconPreviewSuccessful = IconPreviewTask?.Result ?? false;
|
|
||||||
var isLoadingDisplayInfoSuccessful = DisplayInfoTask?.Result ?? false;
|
|
||||||
|
|
||||||
return !isLoadingIconPreviewSuccessful || !isLoadingDisplayInfoSuccessful;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ComputeFolderSize(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Task.Run(
|
|
||||||
async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Special folders like recycle bin don't have a path
|
|
||||||
if (string.IsNullOrWhiteSpace(Item.Path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Dispatcher.RunOnUiThread(_folderSizeDispatcherTimer.Start);
|
|
||||||
GetDirectorySize(new DirectoryInfo(Item.Path), cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError("Failed to calculate folder size", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await Dispatcher.RunOnUiThread(_folderSizeDispatcherTimer.Stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If everything went well, ensure the UI is updated
|
|
||||||
await Dispatcher.RunOnUiThread(UpdateFolderSize);
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GetDirectorySize(DirectoryInfo directory, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var files = directory.GetFiles("*", _fileEnumOptions);
|
|
||||||
for (var i = 0; i < files.Length; i++)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var f = files[i];
|
|
||||||
if (f.Length > 0)
|
|
||||||
{
|
|
||||||
_folderSize += Convert.ToUInt64(f.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var directories = directory.GetDirectories("*", _directoryEnumOptions);
|
|
||||||
for (var i = 0; i < directories.Length; i++)
|
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
ReportProgress(sizeProgress, Item.FileSizeBytes);
|
||||||
GetDirectorySize(directories[i], cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateFolderSize()
|
private void CalculateFolderSizeWithProgress(string path, IProgress<string> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Preview.FileSize = ReadableStringHelper.BytesToReadableString(_folderSize);
|
ulong folderSize = 0;
|
||||||
|
TimeSpan updateInterval = TimeSpan.FromMilliseconds(1000 / MaxUpdateFps);
|
||||||
|
DateTime nextUpdate = DateTime.UtcNow + updateInterval;
|
||||||
|
|
||||||
|
var files = new DirectoryInfo(path).EnumerateFiles("*", FolderEnumerationOptions);
|
||||||
|
|
||||||
|
foreach (var chunk in files.Chunk(FolderEnumerationChunkSize))
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (DateTime.Now >= nextUpdate)
|
||||||
|
{
|
||||||
|
ReportProgress(progress, folderSize);
|
||||||
|
nextUpdate = DateTime.UtcNow + updateInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in chunk)
|
||||||
|
{
|
||||||
|
folderSize += (ulong)file.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportProgress(progress, folderSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FolderSizeDispatcherTimer_Tick(object? sender, object e)
|
private void ReportProgress(IProgress<string> progress, ulong size)
|
||||||
{
|
{
|
||||||
UpdateFolderSize();
|
progress.Report(ReadableStringHelper.BytesToReadableString(size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user