diff --git a/src/AppCommon/FileTime.App.ContainerSizeScanner/ContainerSizeSizeScanProvider.cs b/src/AppCommon/FileTime.App.ContainerSizeScanner/ContainerSizeSizeScanProvider.cs index 15c2562..4e700d0 100644 --- a/src/AppCommon/FileTime.App.ContainerSizeScanner/ContainerSizeSizeScanProvider.cs +++ b/src/AppCommon/FileTime.App.ContainerSizeScanner/ContainerSizeSizeScanProvider.cs @@ -28,7 +28,7 @@ public class ContainerSizeSizeScanProvider : ContentProviderBase, IContainerSize PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, - ItemInitializationSettings itemInitializationSettings = null + ItemInitializationSettings itemInitializationSettings = default ) { if (fullName.Path == ContentProviderName) diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPreStartupHandler.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPreStartupHandler.cs new file mode 100644 index 0000000..1646e40 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPreStartupHandler.cs @@ -0,0 +1,6 @@ +namespace FileTime.App.Core.Services; + +public interface IPreStartupHandler +{ + Task InitAsync(); +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/LifecycleService.cs b/src/AppCommon/FileTime.App.Core/Services/LifecycleService.cs index 3f2e897..91e01a3 100644 --- a/src/AppCommon/FileTime.App.Core/Services/LifecycleService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/LifecycleService.cs @@ -5,15 +5,18 @@ namespace FileTime.App.Core.Services; public class LifecycleService : ILifecycleService { + private readonly IEnumerable _preStartupHandlers; private readonly IEnumerable _exitHandlers; private readonly IEnumerable _startupHandlers; private readonly ILogger _logger; public LifecycleService( + IEnumerable preStartupHandlers, IEnumerable startupHandlers, IEnumerable exitHandlers, ILogger logger) { + _preStartupHandlers = preStartupHandlers; _exitHandlers = exitHandlers; _startupHandlers = startupHandlers; _logger = logger; @@ -21,6 +24,18 @@ public class LifecycleService : ILifecycleService public async Task InitStartupHandlersAsync() { + foreach (var preStartupHandler in _preStartupHandlers) + { + try + { + await preStartupHandler.InitAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while running pre-startup handler {Handler}", preStartupHandler.GetType().FullName); + } + } + foreach (var startupHandler in _startupHandlers) { try @@ -29,7 +44,7 @@ public class LifecycleService : ILifecycleService } catch (Exception ex) { - _logger.LogError(ex, "Error while running startup handler {Handler}", startupHandler?.GetType().FullName); + _logger.LogError(ex, "Error while running startup handler {Handler}", startupHandler.GetType().FullName); } } } diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index 5e366c8..9fadd1c 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -8,6 +8,7 @@ using FileTime.App.Core.Models.Enums; using FileTime.App.Core.UserCommand; using FileTime.App.Core.ViewModels; using FileTime.App.FrequencyNavigation.Services; +using FileTime.Core.ContentAccess; using FileTime.Core.Interactions; using FileTime.Core.Models; using FileTime.Core.Services; @@ -29,6 +30,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private readonly IUserCommunicationService _userCommunicationService; private readonly IFrequencyNavigationService _frequencyNavigationService; private readonly ICommandPaletteService _commandPaletteService; + private readonly IContentProviderRegistry _contentProviderRegistry; private readonly ILogger _logger; private readonly ApplicationConfiguration _applicationConfiguration; private ITabViewModel? _selectedTab; @@ -46,6 +48,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase IUserCommunicationService userCommunicationService, IFrequencyNavigationService frequencyNavigationService, ICommandPaletteService commandPaletteService, + IContentProviderRegistry contentProviderRegistry, ILogger logger, ApplicationConfiguration applicationConfiguration) : base(appState) { @@ -57,6 +60,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase _userCommunicationService = userCommunicationService; _frequencyNavigationService = frequencyNavigationService; _commandPaletteService = commandPaletteService; + _contentProviderRegistry = contentProviderRegistry; _logger = logger; _applicationConfiguration = applicationConfiguration; @@ -249,13 +253,40 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private async Task OpenSelected() { - if (_currentSelectedItem?.Value is not IContainerViewModel containerViewModel || containerViewModel.Container is null) + var targetContainer = await GetSubContainer(); + + if (targetContainer is null + && _currentSelectedItem?.Value is IContainerViewModel {Container: { } container}) + { + targetContainer = container; + } + + if (targetContainer is null) + { return; + } await _appState.SetRapidTravelTextAsync(""); if (_selectedTab?.Tab is { } tab) { - await tab.SetCurrentLocation(containerViewModel.Container); + await tab.SetCurrentLocation(targetContainer); + } + + async Task GetSubContainer() + { + if (_currentSelectedItem?.Value is not {BaseItem: IElement element}) + { + return null; + } + + var subContentProvider = await _contentProviderRegistry.GetSubContentProviderForElement(element); + if (subContentProvider is null) return null; + + var resolvedItem = await subContentProvider.GetItemByFullNameAsync( + element, + new FullName(""), + _timelessContentProvider.CurrentPointInTime.Value!); + return resolvedItem as IContainer; } } diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index 13060d1..6bb1590 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -184,7 +184,9 @@ public partial class TabViewModel : ITabViewModel private static IItem MapItem(AbsolutePath item) { - var t = Task.Run(async () => await MapItemAsync(item)); + var t = Task.Run(async () => + await MapItemAsync(item) + ?? throw new Exception("Could not resolve path " + item.Path.Path)); t.Wait(); return t.Result; } diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 86d5107..5859ae6 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -6,6 +6,7 @@ using FileTime.Core; using FileTime.Providers.Local; using FileTime.Providers.LocalAdmin; using FileTime.Providers.Remote; +using FileTime.Tools.VirtualDiskSources; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -28,6 +29,7 @@ public static class DependencyInjection .AddAppCoreDependencies(configuration) .AddLocalProviderServices() .AddLocalAdminProviderServices(configuration) - .AddRemoteProviderServices(); + .AddRemoteProviderServices() + .AddVirtualDisk(); } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj index 960bc6f..074eac4 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj +++ b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj @@ -20,6 +20,7 @@ + diff --git a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs index 252d269..45575d1 100644 --- a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs +++ b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs @@ -24,7 +24,7 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, - ItemInitializationSettings itemInitializationSettings = null + ItemInitializationSettings itemInitializationSettings = default ) { if (fullName.Path == ContentProviderName) diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs index b152075..f42fbf3 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProviderRegistry.cs @@ -1,10 +1,13 @@ using System.Collections.ObjectModel; +using FileTime.Core.Models; namespace FileTime.Core.ContentAccess; public interface IContentProviderRegistry { ReadOnlyObservableCollection ContentProviders { get; } + ReadOnlyObservableCollection SubContentProviders { get; } void AddContentProvider(IContentProvider contentProvider); void RemoveContentProvider(IContentProvider contentProvider); + Task GetSubContentProviderForElement(IElement parentElement); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/ISubContentProvider.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/ISubContentProvider.cs new file mode 100644 index 0000000..9bc60b4 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/ISubContentProvider.cs @@ -0,0 +1,17 @@ +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Core.ContentAccess; + +public interface ISubContentProvider +{ + Task GetItemByFullNameAsync( + IElement parentElement, + FullName itemPath, + PointInTime pointInTime, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default); + + Task CanHandleAsync(IElement parentElement); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs b/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs index cbd2ec7..958f476 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/AbsolutePath.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using FileTime.Core.Enums; using FileTime.Core.Timeline; namespace FileTime.Core.Models; +[DebuggerDisplay("{Path.Path}, {Type}")] public class AbsolutePath { public ITimelessContentProvider TimelessProvider { get; } diff --git a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs index 343cb4b..d0bbc6b 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs @@ -3,4 +3,5 @@ namespace FileTime.Core.Models; public static class Constants { public const char SeparatorChar = '/'; + public const string SubContentProviderRootContainer = ":"; } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs index 9dde8e1..c73eb86 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs @@ -1,5 +1,8 @@ +using System.Diagnostics; + namespace FileTime.Core.Models; +[DebuggerDisplay("{Path}")] public record FullName(string Path) { public FullName? GetParent() diff --git a/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs index 13c24ba..f102978 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/ItemInitializationSettings.cs @@ -1,6 +1,6 @@ namespace FileTime.Core.Models; -public sealed class ItemInitializationSettings +public readonly struct ItemInitializationSettings { public bool SkipChildInitialization { get; init; } public AbsolutePath? Parent { get; init; } diff --git a/src/Core/FileTime.Core.Abstraction/Models/NativePath.cs b/src/Core/FileTime.Core.Abstraction/Models/NativePath.cs index d8c9310..407fa62 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/NativePath.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/NativePath.cs @@ -1,5 +1,8 @@ +using System.Diagnostics; + namespace FileTime.Core.Models; +[DebuggerDisplay("{Path}")] public record NativePath(string Path) { public override string ToString() => Path; diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs index 21c3234..cece806 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs @@ -1,4 +1,4 @@ -using System.Reactive.Subjects; +using DeclarativeProperty; using FileTime.Core.Enums; using FileTime.Core.Models; @@ -6,7 +6,7 @@ namespace FileTime.Core.Timeline; public interface ITimelessContentProvider { - BehaviorSubject CurrentPointInTime { get; } + IDeclarativeProperty CurrentPointInTime { get; } Task GetItemByFullNameAsync(FullName fullName, PointInTime? pointInTime, diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs index bf25557..a7d0b60 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderRegistry.cs @@ -1,4 +1,7 @@ using System.Collections.ObjectModel; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; using Microsoft.Extensions.DependencyInjection; namespace FileTime.Core.ContentAccess; @@ -8,13 +11,16 @@ public class ContentProviderRegistry : IContentProviderRegistry private readonly object _lock = new(); private readonly IServiceProvider _serviceProvider; private readonly ObservableCollection _contentProviders = new(); + private readonly ObservableCollection _subContentProviders = new(); private readonly ReadOnlyObservableCollection _contentProvidersReadOnly; + private readonly ReadOnlyObservableCollection _subContentProvidersReadOnly; private bool _initialized; public ContentProviderRegistry(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; _contentProvidersReadOnly = new ReadOnlyObservableCollection(_contentProviders); + _subContentProvidersReadOnly = new ReadOnlyObservableCollection(_subContentProviders); } public ReadOnlyObservableCollection ContentProviders @@ -26,20 +32,33 @@ public class ContentProviderRegistry : IContentProviderRegistry } } + public ReadOnlyObservableCollection SubContentProviders + { + get + { + InitializeContentProviderListIfNeeded(); + return _subContentProvidersReadOnly; + } + } + private void InitializeContentProviderListIfNeeded() { if (_initialized) return; lock (_lock) { - if (!_initialized) - { - foreach (var contentProvider in _serviceProvider.GetServices()) - { - _contentProviders.Add(contentProvider); - } + if (_initialized) return; - _initialized = true; + foreach (var contentProvider in _serviceProvider.GetServices()) + { + _contentProviders.Add(contentProvider); } + + foreach (var subContentProvider in _serviceProvider.GetServices()) + { + _subContentProviders.Add(subContentProvider); + } + + _initialized = true; } } @@ -62,4 +81,20 @@ public class ContentProviderRegistry : IContentProviderRegistry _contentProviders.Remove(contentProvider); } } + + public async Task GetSubContentProviderForElement(IElement parentElement) + { + var subContentProviders = _serviceProvider + .GetServices() + .ToList(); + + foreach (var subContentProvider in subContentProviders) + { + if(!await subContentProvider.CanHandleAsync(parentElement)) continue; + + return subContentProvider; + } + + return null; + } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/SubContentProviderBase.cs b/src/Core/FileTime.Core.ContentAccess/SubContentProviderBase.cs new file mode 100644 index 0000000..01b9a57 --- /dev/null +++ b/src/Core/FileTime.Core.ContentAccess/SubContentProviderBase.cs @@ -0,0 +1,46 @@ +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; + +namespace FileTime.Core.ContentAccess; + +public abstract class SubContentProviderBase : ContentProviderBase +{ + private readonly IContentProvider _parentContentProvider; + + protected SubContentProviderBase( + IContentProvider parentContentProvider, + string name, + ITimelessContentProvider timelessContentProvider) : base(name, timelessContentProvider) + { + _parentContentProvider = parentContentProvider; + } + + public override async Task GetItemByNativePathAsync( + NativePath nativePath, + PointInTime pointInTime, + bool forceResolve = false, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) + => await _parentContentProvider.GetItemByNativePathAsync( + nativePath, + pointInTime, + forceResolve, + forceResolvePathType, + itemInitializationSettings); + + public override async ValueTask GetNativePathAsync(FullName fullName) + => await _parentContentProvider.GetNativePathAsync(fullName); + + public override FullName GetFullName(NativePath nativePath) + => _parentContentProvider.GetFullName(nativePath); + + public override async Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default) + => await _parentContentProvider.GetContentAsync(element, maxLength, cancellationToken); + + public override async Task CanHandlePathAsync(NativePath path) + => await _parentContentProvider.CanHandlePathAsync(path); + + public override VolumeSizeInfo? GetVolumeSizeInfo(FullName path) + => _parentContentProvider.GetVolumeSizeInfo(path); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs index d04ea7f..1a805d5 100644 --- a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs @@ -1,4 +1,4 @@ -using System.Reactive.Subjects; +using DeclarativeProperty; using FileTime.Core.ContentAccess; using FileTime.Core.Enums; using FileTime.Core.Models; @@ -10,8 +10,8 @@ public class TimelessContentProvider : ITimelessContentProvider { private readonly IContentProviderRegistry _contentProviderRegistry; private readonly Lazy _rootContentProvider; - - public BehaviorSubject CurrentPointInTime { get; } = new(PointInTime.Present); + private readonly DeclarativeProperty _currentPointInTime = new(PointInTime.Present); + public IDeclarativeProperty CurrentPointInTime => _currentPointInTime; public TimelessContentProvider( IContentProviderRegistry contentProviderRegistry, diff --git a/src/FileTime.sln b/src/FileTime.sln index fc83526..e395723 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -147,6 +147,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.Tracker", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Server.Extensions.DependencyInjection", "Server\FileTime.Server.Extensions.DependencyInjection\FileTime.Server.Extensions.DependencyInjection.csproj", "{25AA9F04-EEEE-49C4-870B-CDFF71717687}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskSources", "Tools\FileTime.Tools.VirtualDiskSources\FileTime.Tools.VirtualDiskSources.csproj", "{DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.Tools.VirtualDiskSources.Abstractions", "Tools\FileTime.Tools.VirtualDiskSources.Abstractions\FileTime.Tools.VirtualDiskSources.Abstractions.csproj", "{53E5B762-B620-4106-B481-31A478A1E14F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -405,6 +409,14 @@ Global {25AA9F04-EEEE-49C4-870B-CDFF71717687}.Debug|Any CPU.Build.0 = Debug|Any CPU {25AA9F04-EEEE-49C4-870B-CDFF71717687}.Release|Any CPU.ActiveCfg = Release|Any CPU {25AA9F04-EEEE-49C4-870B-CDFF71717687}.Release|Any CPU.Build.0 = Release|Any CPU + {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9}.Release|Any CPU.Build.0 = Release|Any CPU + {53E5B762-B620-4106-B481-31A478A1E14F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53E5B762-B620-4106-B481-31A478A1E14F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53E5B762-B620-4106-B481-31A478A1E14F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -474,6 +486,8 @@ Global {0A29616B-5413-4DA9-96EC-B45D6AF632C9} = {3324D046-1E05-46B5-B1BA-82910D56B332} {BD382403-86D8-4E99-B279-370FC22F059C} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} {25AA9F04-EEEE-49C4-870B-CDFF71717687} = {778AAF38-20FF-438C-A9C3-60850C8B5A27} + {DBCB10AD-9647-46AB-B9A1-3ACB9BCA46B9} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} + {53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Program.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Program.cs index 7db8e00..2e523fa 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Program.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Program.cs @@ -113,9 +113,9 @@ public static class Program private static void NormalStartup(string[] args) { - AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException; + /*AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException; AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException; - TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException; + TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException;*/ try { BuildAvaloniaApp() diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 8647497..bfa9284 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -12,15 +12,18 @@ namespace FileTime.Providers.Local; public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider { private readonly ITimelessContentProvider _timelessContentProvider; + private readonly IContentProviderRegistry _contentProviderRegistry; private readonly bool _isCaseInsensitive; private readonly Lazy> _rootDriveInfos; public LocalContentProvider( ITimelessContentProvider timelessContentProvider, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IContentProviderRegistry contentProviderRegistry) : base(LocalContentProviderConstants.ContentProviderId, timelessContentProvider) { _timelessContentProvider = timelessContentProvider; + _contentProviderRegistry = contentProviderRegistry; _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); _rootDriveInfos = new Lazy>(() => serviceProvider.GetRequiredService().RootDriveInfos); @@ -45,12 +48,6 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo Items.Clear(); Items.AddRange(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present))); - - /*Items.Edit(actions => - { - actions.Clear(); - actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present))); - });*/ } public override async Task CanHandlePathAsync(NativePath path) @@ -79,11 +76,11 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo return new VolumeSizeInfo(rootDriveInfo.Size, rootDriveInfo.Free); } - public override Task GetItemByNativePathAsync(NativePath nativePath, + public override async Task GetItemByNativePathAsync(NativePath nativePath, PointInTime pointInTime, bool forceResolve = false, AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, - ItemInitializationSettings? itemInitializationSettings = null) + ItemInitializationSettings itemInitializationSettings = default) { var path = nativePath.Path; Exception? innerException; @@ -91,19 +88,40 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo { if (path.Length == 0) { - return Task.FromResult((IItem) this); + return this; } - else if (Directory.Exists(path)) + + if (Directory.Exists(path)) { - return Task.FromResult((IItem) DirectoryToContainer( - new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar), + return DirectoryToContainer( + new DirectoryInfo(path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar), pointInTime, - itemInitializationSettings) - ); + itemInitializationSettings); } else if (File.Exists(path)) { - return Task.FromResult((IItem) FileToElement(new FileInfo(path), pointInTime)); + return FileToElement(new FileInfo(path), pointInTime); + } + + var pathParts = path.Split(Path.DirectorySeparatorChar).SelectMany(p => p.Split(Constants.SeparatorChar)).ToArray(); + + for (var i = pathParts.Length - 1; i > 0; i--) + { + var possibleFile = string.Join(Path.DirectorySeparatorChar, pathParts.Take(i)); + if (!File.Exists(possibleFile)) continue; + + var element = FileToElement(new FileInfo(possibleFile), pointInTime); + var subContentProvider = await _contentProviderRegistry.GetSubContentProviderForElement(element); + if (subContentProvider is null) break; + + var subPath = string.Join(Constants.SeparatorChar, pathParts.Skip(i)); + + var resolvedItem = await subContentProvider.GetItemByFullNameAsync(element, new FullName(subPath), pointInTime, forceResolvePathType, itemInitializationSettings); + + if (resolvedItem is not null) + { + return resolvedItem; + } } var type = forceResolvePathType switch @@ -134,14 +152,12 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo return forceResolvePathType switch { - AbsolutePathType.Container => Task.FromResult( - (IItem) CreateEmptyContainer( - nativePath, - pointInTime, - new List() {innerException} - ) + AbsolutePathType.Container => CreateEmptyContainer( + nativePath, + pointInTime, + new List {innerException} ), - AbsolutePathType.Element => Task.FromResult(CreateEmptyElement(nativePath)), + AbsolutePathType.Element => CreateEmptyElement(nativePath), _ => throw new Exception( $"Could not resolve path '{nativePath.Path}' and could not force create, because {nameof(forceResolvePathType)} is {nameof(AbsolutePathType.Unknown)}.", innerException) @@ -211,10 +227,8 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo } private Container DirectoryToContainer(DirectoryInfo directoryInfo, PointInTime pointInTime, - ItemInitializationSettings? initializationSettings = null) + ItemInitializationSettings initializationSettings = default) { - initializationSettings ??= new(); - var fullName = GetFullName(directoryInfo.FullName); var parentFullName = fullName.GetParent(); var parent = @@ -336,17 +350,6 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo } } - private List GetItemsByContainer(DirectoryInfo directoryInfo, PointInTime pointInTime) - => directoryInfo - .GetDirectories() - .Select(d => DirectoryToAbsolutePath(d, pointInTime)) - .Concat( - directoryInfo - .GetFiles() - .Select(f => FileToAbsolutePath(f, pointInTime)) - ) - .ToList(); - private Element FileToElement(FileInfo fileInfo, PointInTime pointInTime) { var fullName = GetFullName(fileInfo); @@ -387,7 +390,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo nativePath.TrimStart(Constants.SeparatorChar).Split(Path.DirectorySeparatorChar))) .TrimEnd(Constants.SeparatorChar))!; - public override ValueTask GetNativePathAsync(FullName fullName) + public override ValueTask GetNativePathAsync(FullName fullName) => ValueTask.FromResult(GetNativePath(fullName)); public NativePath GetNativePath(FullName fullName) diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/FileTime.Tools.VirtualDiskSources.Abstractions.csproj b/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/FileTime.Tools.VirtualDiskSources.Abstractions.csproj new file mode 100644 index 0000000..6a5c619 --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/FileTime.Tools.VirtualDiskSources.Abstractions.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/IVirtualDiskSubContentProvider.cs b/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/IVirtualDiskSubContentProvider.cs new file mode 100644 index 0000000..32a6453 --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources.Abstractions/IVirtualDiskSubContentProvider.cs @@ -0,0 +1,8 @@ +using FileTime.Core.ContentAccess; + +namespace FileTime.Tools.VirtualDiskSources.Abstractions; + +public interface IVirtualDiskSubContentProvider : ISubContentProvider +{ + +} \ No newline at end of file diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources/DiscUtilsInitializer.cs b/src/Tools/FileTime.Tools.VirtualDiskSources/DiscUtilsInitializer.cs new file mode 100644 index 0000000..498aa30 --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources/DiscUtilsInitializer.cs @@ -0,0 +1,14 @@ +using FileTime.App.Core.Services; + +namespace FileTime.Tools.VirtualDiskSources; + +public class DiscUtilsInitializer : IPreStartupHandler +{ + public Task InitAsync() + { + DiscUtils.Containers.SetupHelper.SetupContainers(); + DiscUtils.FileSystems.SetupHelper.SetupFileSystems(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources/FileTime.Tools.VirtualDiskSources.csproj b/src/Tools/FileTime.Tools.VirtualDiskSources/FileTime.Tools.VirtualDiskSources.csproj new file mode 100644 index 0000000..2e17319 --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources/FileTime.Tools.VirtualDiskSources.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources/Startup.cs b/src/Tools/FileTime.Tools.VirtualDiskSources/Startup.cs new file mode 100644 index 0000000..2ee099e --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources/Startup.cs @@ -0,0 +1,18 @@ +using FileTime.App.Core.Services; +using FileTime.Core.ContentAccess; +using FileTime.Tools.VirtualDiskSources.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace FileTime.Tools.VirtualDiskSources; + +public static class Startup +{ + public static IServiceCollection AddVirtualDisk(this IServiceCollection services) + { + services.TryAddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + return services; + } +} \ No newline at end of file diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskContentProvider.cs b/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskContentProvider.cs new file mode 100644 index 0000000..b892a0e --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskContentProvider.cs @@ -0,0 +1,14 @@ +using FileTime.Core.ContentAccess; +using FileTime.Core.Timeline; + +namespace FileTime.Tools.VirtualDiskSources; + +public class VirtualDiskContentProvider : SubContentProviderBase +{ + public VirtualDiskContentProvider( + IContentProvider parentContentProvider, + ITimelessContentProvider timelessContentProvider) + : base(parentContentProvider, "virtual-disk", timelessContentProvider) + { + } +} \ No newline at end of file diff --git a/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskSubContentProvider.cs b/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskSubContentProvider.cs new file mode 100644 index 0000000..ea2ca6d --- /dev/null +++ b/src/Tools/FileTime.Tools.VirtualDiskSources/VirtualDiskSubContentProvider.cs @@ -0,0 +1,220 @@ +using System.Collections.ObjectModel; +using DiscUtils; +using DiscUtils.Iso9660; +using DiscUtils.Udf; +using FileTime.Core.ContentAccess; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; +using FileTime.Tools.VirtualDiskSources.Abstractions; + +namespace FileTime.Tools.VirtualDiskSources; + +public class VirtualDiskSubContentProvider : IVirtualDiskSubContentProvider +{ + private readonly IContentAccessorFactory _contentAccessorFactory; + private readonly ITimelessContentProvider _timelessContentProvider; + + public VirtualDiskSubContentProvider( + IContentAccessorFactory contentAccessorFactory, + ITimelessContentProvider timelessContentProvider + ) + { + _contentAccessorFactory = contentAccessorFactory; + _timelessContentProvider = timelessContentProvider; + } + + public Task CanHandleAsync(IElement parentElement) + => Task.FromResult(parentElement.NativePath?.Path.EndsWith(".iso", StringComparison.OrdinalIgnoreCase) ?? false); + + public async Task GetItemByFullNameAsync( + IElement parentElement, + FullName itemPath, + PointInTime pointInTime, + AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, + ItemInitializationSettings itemInitializationSettings = default) + { + var contentReaderFactory = _contentAccessorFactory.GetContentReaderFactory(parentElement.Provider); + var reader = await contentReaderFactory.CreateContentReaderAsync(parentElement); + + await using var readerStream = reader.AsStream(); + var discReader = new UdfReader(readerStream); + + if (itemPath.Path.Length == 0 || itemPath.Path == Constants.SubContentProviderRootContainer) + { + var rootFullNameBase = parentElement.FullName!.Path + Constants.SeparatorChar + Constants.SubContentProviderRootContainer; + var rootNativePathBase = parentElement.NativePath!.Path + Constants.SeparatorChar + Constants.SubContentProviderRootContainer; + + var rootFullName = new FullName(rootFullNameBase); + var rootNativePath = new NativePath(rootNativePathBase); + + return CreateContainer(discReader.Root, + rootFullName, + rootNativePath, + parentElement.Provider, + parentElement.Parent!, + parentElement.PointInTime, + itemInitializationSettings); + } + + return ResolveNonRootChild(discReader, parentElement, itemPath, pointInTime, itemInitializationSettings); + } + + private IItem? ResolveNonRootChild( + UdfReader discReader, + IElement parentElement, + FullName itemPath, + PointInTime pointInTime, + ItemInitializationSettings itemInitializationSettings = default) + { + var pathParts = itemPath.Path.Split(Constants.SeparatorChar); + + var childFullNameBase = parentElement.FullName!.Path + Constants.SeparatorChar + itemPath.Path; + var childNativePathBase = parentElement.NativePath!.Path + Constants.SeparatorChar + itemPath.Path; + + var childFullName = new FullName(childFullNameBase); + var childNativePath = new NativePath(childNativePathBase); + + var parent = new AbsolutePath(_timelessContentProvider, pointInTime, childFullName.GetParent()!, AbsolutePathType.Container); + + var container = discReader.Root; + for (var i = 1; i < pathParts.Length - 1; i++) + { + if (container is null) break; + container = container.GetDirectories().FirstOrDefault(d => d.Name == pathParts[i]); + } + + if (container is null) return null; + + if (container.GetDirectories().FirstOrDefault(d => d.Name == pathParts[^1]) is { } childContainer) + { + return CreateContainer( + childContainer, + childFullName, + childNativePath, + parentElement.Provider, + parent, + pointInTime, + itemInitializationSettings + ); + } + + if (container.GetFiles().FirstOrDefault(d => d.Name == pathParts[^1]) is { } childElement) + { + return CreateElement( + childElement, + childFullName, + childNativePath, + parentElement.Provider, + parent, + pointInTime + ); + } + + return null; + } + + private IContainer CreateContainer( + DiscDirectoryInfo sourceContainer, + FullName fullname, + NativePath nativePath, + IContentProvider parentContentProvider, + AbsolutePath parent, + PointInTime pointInTime, + ItemInitializationSettings initializationSettings) + { + var children = new ObservableCollection(); + var exceptions = new ObservableCollection(); + var container = new Container( + sourceContainer.Name, + sourceContainer.Name, + fullname, + nativePath, + parent, + true, + true, + sourceContainer.CreationTime, + sourceContainer.LastWriteTime, + SupportsDelete.False, + false, + FormatAttributes(sourceContainer.Attributes), + new VirtualDiskContentProvider(parentContentProvider, _timelessContentProvider), + false, + pointInTime, + exceptions, + new ReadOnlyExtensionCollection(new ExtensionCollection()), + children + ); + + if (!initializationSettings.SkipChildInitialization) + { + ThreadPool.QueueUserWorkItem(_ => LoadChildren(container, sourceContainer, children, pointInTime, exceptions)); + } + + return container; + } + + private void LoadChildren( + Container container, + DiscDirectoryInfo sourceContainer, + ObservableCollection children, + PointInTime pointInTime, + ObservableCollection exceptions + ) + { + foreach (var discDirectoryInfo in sourceContainer.GetDirectories()) + { + children.Add(new AbsolutePath( + _timelessContentProvider, + pointInTime, + container.FullName.GetChild(discDirectoryInfo.Name), + AbsolutePathType.Container) + ); + } + + foreach (var fileInfo in sourceContainer.GetFiles()) + { + children.Add(new AbsolutePath( + _timelessContentProvider, + pointInTime, + container.FullName.GetChild(fileInfo.Name), + AbsolutePathType.Element) + ); + } + } + + private IElement CreateElement(DiscFileInfo childElement, + FullName fullname, + NativePath nativePath, + IContentProvider parentContentProvider, + AbsolutePath parent, + PointInTime pointInTime) + { + var element = new Element( + childElement.Name, + childElement.Name, + fullname, + nativePath, + parent, + true, + true, + childElement.CreationTime, + childElement.LastWriteTime, + SupportsDelete.False, + false, + FormatAttributes(childElement.Attributes), + childElement.Length, + new VirtualDiskContentProvider(parentContentProvider, _timelessContentProvider), + pointInTime, + new ObservableCollection(), + new ReadOnlyExtensionCollection(new ExtensionCollection()) + ); + + return element; + } + + private string FormatAttributes(FileAttributes attributes) + { + return ""; + } +} \ No newline at end of file