using Explorer.Plugin.Abstractions; using Explorer.Windows; namespace Explorer.Plugin.OneDrive; public sealed class OneDriveStorageProvider : IStorageProvider { public const string ProviderId = "onedrive"; private readonly object _gate = new(); private IReadOnlyList? _places; public ProviderManifest Manifest { get; } = new( ProviderId, "OneDrive", "1.0.0", ProviderIsolation.InProcess); public ProviderCapability GetCapabilities() => ProviderCapability.CloudState | ProviderCapability.Pin | ProviderCapability.Dehydrate; public bool TryMatchRoot(string path) { if (string.IsNullOrWhiteSpace(path)) { return false; } foreach (var root in GetPlaces()) { if (IsUnder(root.Path, path)) { return true; } } return CloudFilesNative.IsCloudSyncRoot(path); } public Task> GetItemStatesAsync( IReadOnlyList paths, CancellationToken cancellationToken = default) { var results = new List(paths.Count); foreach (var path in paths) { cancellationToken.ThrowIfCancellationRequested(); if (!TryMatchRoot(path)) { continue; } try { var attrs = (int)File.GetAttributes(path); var isDir = (attrs & (int)FileAttributes.Directory) != 0; long logical = 0; if (!isDir) { try { logical = new FileInfo(path).Length; } catch { /* locked */ } } var allocated = CloudFilesNative.TryGetAllocatedSize(path); var placeholder = CloudFilesNative.TryGetPlaceholderState(attrs, 0); results.Add(OneDrivePlaceholderState.FromLocalSignals( ProviderId, path, attrs, 0, logical, allocated, placeholder)); } catch { // fail-open for this item } } return Task.FromResult>(results); } public Task TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default) { var capabilities = GetCapabilities(); var needed = request.Action switch { ProviderAction.Pin or ProviderAction.Unpin => ProviderCapability.Pin, ProviderAction.Dehydrate => ProviderCapability.Dehydrate, _ => ProviderCapability.None }; if (needed == ProviderCapability.None || (capabilities & needed) == 0) { return Task.FromResult(new ProviderActionResult(ProviderActionStatus.Unsupported, "This action is not available.")); } var pin = request.Action switch { ProviderAction.Pin => CloudFilesNative.PinPinned, ProviderAction.Unpin or ProviderAction.Dehydrate => CloudFilesNative.PinUnpinned, _ => CloudFilesNative.PinUnspecified }; var failed = 0; foreach (var path in request.Paths) { cancellationToken.ThrowIfCancellationRequested(); if (!CloudFilesNative.TrySetPinState(path, pin)) { failed++; } } if (failed == 0) { return Task.FromResult(new ProviderActionResult(ProviderActionStatus.Succeeded)); } if (failed == request.Paths.Count) { return Task.FromResult(new ProviderActionResult(ProviderActionStatus.Failed, "OneDrive could not change the pin state.")); } return Task.FromResult(new ProviderActionResult(ProviderActionStatus.Failed, $"OneDrive could not change the pin state for {failed} item(s).")); } public Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) => Task.FromResult(null); public IReadOnlyList GetPlaces() { lock (_gate) { return _places ??= OneDrivePlaceDiscovery.Discover(); } } private static bool IsUnder(string root, string path) { var a = root.TrimEnd('\\'); var b = path.TrimEnd('\\'); return b.Equals(a, StringComparison.OrdinalIgnoreCase) || b.StartsWith(a + "\\", StringComparison.OrdinalIgnoreCase); } }