using Explorer.Domain; using Explorer.Plugin.Abstractions; using Microsoft.Extensions.Logging; namespace Explorer.Application; public sealed class StorageProviderRegistry { private readonly IReadOnlyList _providers; private readonly HashSet _disabled = new(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; public StorageProviderRegistry(IEnumerable providers, ILogger logger) { _providers = providers.ToList(); _logger = logger; } public IReadOnlyList Providers => _providers; public void SetEnabled(string providerId, bool enabled) { if (enabled) { _disabled.Remove(providerId); } else { _disabled.Add(providerId); } } public IStorageProvider? Find(string path) { foreach (var provider in _providers) { if (_disabled.Contains(provider.Manifest.Id)) { continue; } try { if (provider.TryMatchRoot(path)) { return provider; } } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed matching {Path}", provider.Manifest.Id, path); } } return null; } public async Task> EnrichAsync( IReadOnlyList items, CancellationToken cancellationToken = default) { if (items.Count == 0 || _providers.Count == 0) { return items; } var groups = new Dictionary>(); for (var i = 0; i < items.Count; i++) { var provider = Find(items[i].FullPath); if (provider is null) { continue; } if (!groups.TryGetValue(provider, out var list)) { list = []; groups[provider] = list; } list.Add(i); } if (groups.Count == 0) { return items; } var copy = items.ToArray(); foreach (var (provider, indexes) in groups) { try { var paths = indexes.Select(i => copy[i].FullPath).ToList(); var states = await provider.GetItemStatesAsync(paths, cancellationToken).ConfigureAwait(false); var byPath = states.ToDictionary(s => s.Path, StringComparer.OrdinalIgnoreCase); foreach (var index in indexes) { if (byPath.TryGetValue(copy[index].FullPath, out var state)) { copy[index] = CloudPresenceMapper.Apply(copy[index], state); } } } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed enriching items", provider.Manifest.Id); } } return copy; } public bool HasCapability(string path, ProviderCapability capability) { var provider = Find(path); return provider is not null && (provider.GetCapabilities() & capability) != 0; } public IReadOnlyList GetPlaces() { var places = new List(); foreach (var provider in _providers) { if (_disabled.Contains(provider.Manifest.Id)) { continue; } try { places.AddRange(provider.GetPlaces()); } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed listing places", provider.Manifest.Id); } } return places; } public async Task InvokeAsync( ProviderAction action, IReadOnlyList paths, CancellationToken cancellationToken = default) { if (paths.Count == 0) { return new ProviderActionResult(ProviderActionStatus.Failed, "Nothing selected."); } var provider = Find(paths[0]); if (provider is null) { return new ProviderActionResult(ProviderActionStatus.Unsupported, "No cloud provider is available for this location."); } try { return await provider.TryInvokeAsync(new ProviderActionRequest(action, paths), cancellationToken) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed invoking {Action}", provider.Manifest.Id, action); return new ProviderActionResult(ProviderActionStatus.Failed, "The cloud provider could not complete this action."); } } public async Task GetStateAsync(string path, CancellationToken cancellationToken = default) { var provider = Find(path); if (provider is null) { return null; } try { var states = await provider.GetItemStatesAsync([path], cancellationToken).ConfigureAwait(false); return states.FirstOrDefault(); } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed reading state for {Path}", provider.Manifest.Id, path); return null; } } public async Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) { var provider = Find(rootPath); if (provider is null) { return null; } try { return await provider.TryGetQuotaAsync(rootPath, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.LogDebug(ex, "Provider {Id} failed reading quota for {Path}", provider.Manifest.Id, rootPath); return null; } } } public static class CloudPresenceMapper { public static FileSystemItem Apply(FileSystemItem item, ProviderItemState state) => item.Overlay( allocatedSizeBytes: state.AllocatedSizeBytes ?? item.AllocatedSizeBytes, cloud: ToPresence(state), hydration: item.Hydration | ItemHydrationFlags.Provider); public static CloudPresence ToPresence(ProviderItemState state) => new( state.ProviderId, (Domain.CloudAvailability)(int)state.Availability, state.LogicalSizeBytes, state.AllocatedSizeBytes, state.MayHydrateOnRead, state.StatusText); public static void ApplyToEntry(IndexEntry entry, CloudPresence? cloud) { if (cloud is null) { return; } entry.AllocatedSizeBytes = cloud.AllocatedSizeBytes; entry.CloudAvailability = cloud.Availability; } }