using dy.net.model.dto; using dy.net.model.entity; using dy.net.service; namespace dy.net.storage { public sealed class OpenListMediaStorage : IMediaStorage, ICanonicalMediaStorage { private readonly OpenListSettingsService _settingsService; private readonly OpenListClient _client; private readonly OpenListTransferService _transfers; public OpenListMediaStorage( OpenListSettingsService settingsService, OpenListClient client, OpenListTransferService transfers) { _settingsService = settingsService; _client = client; _transfers = transfers; } public StorageType StorageType => StorageType.OpenList; public async Task CanonicalizePathAsync( string path, bool createParentDirectories = false, CancellationToken cancellationToken = default) { var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); var actual = await _client.ResolveCanonicalObjectPathAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), createParentDirectories, cancellationToken); return OpenListTransferService.ToLogicalPath(settings, actual); } public async Task EnsureDirectoryAsync(string path, CancellationToken cancellationToken = default) { var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); await _client.EnsureDirectoryAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), cancellationToken); } public async Task ExistsAsync(string path, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path)) return false; var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); var info = await _client.TryGetObjectAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), cancellationToken); return info is { IsDirectory: false }; } public async Task GetLengthAsync(string path, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path)) return null; var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); var info = await _client.TryGetObjectAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), cancellationToken); return info is { IsDirectory: false } ? info.Size : null; } public Task WriteAsync( string path, Stream source, long? contentLength = null, string contentType = null, CancellationToken cancellationToken = default) => _transfers.TransferAsync(path, source, contentLength, cancellationToken); public async Task OpenReadAsync( string path, long? from = null, long? to = null, CancellationToken cancellationToken = default) { var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); return await _client.OpenReadAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), from, to, cancellationToken); } public async Task DeleteAsync(string path, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path)) return; var settings = await _settingsService.GetAsync(); var password = await RequirePasswordAsync(settings); await _client.DeleteObjectAsync(settings, password, OpenListTransferService.ToActualPath(settings, path), cancellationToken); } public IMediaStorage Bind(OpenListSettings settings) { OpenListTransferService.ValidateSettings(settings); return new BoundOpenListMediaStorage(this, settings); } public async Task ListDirectoriesAsync( OpenListSettings settings, string path, CancellationToken cancellationToken = default) { OpenListTransferService.ValidateSettings(settings); var password = await RequirePasswordAsync(settings); return await _client.ListDirectoriesAsync(settings, password, path, true, cancellationToken); } /// /// 验证登录、本地目录与 OpenList 源挂载映射、服务端复制、Range 读取和删除。 /// public async Task<(bool Success, string Message)> ProbeAsync( OpenListSettings settings, CancellationToken cancellationToken = default) { string localDirectory = null; string targetDirectory = null; string password = null; try { OpenListTransferService.ValidateSettings(settings); password = await RequirePasswordAsync(settings); var connectionMessage = await _client.ProbeAsync(settings, password, cancellationToken); var id = Guid.NewGuid().ToString("N"); var bytes = System.Text.Encoding.UTF8.GetBytes("dysync-openlist-copy-probe"); localDirectory = Path.Combine(Path.GetFullPath(settings.LocalStagingPath), id); Directory.CreateDirectory(localDirectory); var localPath = Path.Combine(localDirectory, "probe.txt"); await File.WriteAllBytesAsync(localPath, bytes, cancellationToken); var sourcePath = StoragePath.CombineRemote(settings.SourcePath, id, "probe.txt"); targetDirectory = StoragePath.CombineRemote(settings.BasePath, $".dysync-probe-{id}"); var targetPath = StoragePath.CombineRemote(targetDirectory, "probe.txt"); OpenListObjectInfo source = null; foreach (var delay in new[] { 0, 500, 1000, 2000, 4000 }) { if (delay > 0) await Task.Delay(delay, cancellationToken); source = await _client.TryGetObjectAsync(settings, password, sourcePath, cancellationToken); if (source is { IsDirectory: false } && source.Size == bytes.Length) break; } if (source is not { IsDirectory: false } || source.Size != bytes.Length) throw new InvalidOperationException( $"OpenList 无法从源挂载目录看到测试文件。请确认本地目录“{settings.LocalStagingPath}”映射到 OpenList“{settings.SourcePath}”。"); await _client.EnsureDirectoryAsync(settings, password, targetDirectory, cancellationToken); var copy = await _client.CopyFileAsync(settings, password, sourcePath, targetPath, cancellationToken); var taskId = copy.TaskIds.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(taskId)) { var deadline = DateTime.UtcNow.AddMinutes(2); while (DateTime.UtcNow < deadline) { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); var task = await _client.TryGetCopyTaskAsync(settings, password, taskId, cancellationToken); if (task?.State == 2) break; if (task?.State is 4 or 7) throw new IOException(task.Error ?? $"OpenList 复制任务失败:state={task.State}"); } } var target = await _client.TryGetObjectAsync(settings, password, targetPath, cancellationToken); if (target is not { IsDirectory: false } || target.Size != bytes.Length) throw new IOException($"OpenList 服务端复制校验失败:期望 {bytes.Length},实际 {target?.Size.ToString() ?? "不存在"}。"); await using (var read = await _client.OpenReadAsync(settings, password, targetPath, 0, 0, cancellationToken)) { var oneByte = new byte[1]; if (await read.Stream.ReadAsync(oneByte.AsMemory(0, 1), cancellationToken) != 1) throw new IOException("OpenList Range 读取未返回数据。"); } await _client.DeleteObjectAsync(settings, password, targetDirectory, cancellationToken); return (true, $"{connectionMessage} 服务端复制、Range 与删除能力正常。"); } catch (Exception ex) { Serilog.Log.Warning(ex, "OpenList 完整能力探测失败"); return (false, ex.GetBaseException().Message); } finally { if (!string.IsNullOrWhiteSpace(targetDirectory) && !string.IsNullOrWhiteSpace(password)) { try { await _client.DeleteObjectAsync(settings, password, targetDirectory, CancellationToken.None); } catch { } } if (!string.IsNullOrWhiteSpace(localDirectory) && Directory.Exists(localDirectory)) { try { Directory.Delete(localDirectory, true); } catch { } } } } private async Task RequirePasswordAsync(OpenListSettings settings) { var password = await _settingsService.GetPasswordAsync(settings); if (string.IsNullOrWhiteSpace(password)) throw new InvalidOperationException("OpenList 密码无效,请重新输入并保存。"); return password; } private sealed class BoundOpenListMediaStorage : IMediaStorage, ICanonicalMediaStorage { private readonly OpenListMediaStorage _owner; private readonly OpenListSettings _settings; public BoundOpenListMediaStorage(OpenListMediaStorage owner, OpenListSettings settings) { _owner = owner; _settings = settings; } public StorageType StorageType => StorageType.OpenList; public async Task CanonicalizePathAsync( string path, bool createParentDirectories = false, CancellationToken cancellationToken = default) { var password = await _owner.RequirePasswordAsync(_settings); var actual = await _owner._client.ResolveCanonicalObjectPathAsync(_settings, password, OpenListTransferService.ToActualPath(_settings, path), createParentDirectories, cancellationToken); return OpenListTransferService.ToLogicalPath(_settings, actual); } public async Task EnsureDirectoryAsync(string path, CancellationToken cancellationToken = default) { var password = await _owner.RequirePasswordAsync(_settings); await _owner._client.EnsureDirectoryAsync(_settings, password, OpenListTransferService.ToActualPath(_settings, path), cancellationToken); } public async Task ExistsAsync(string path, CancellationToken cancellationToken = default) => await GetLengthAsync(path, cancellationToken) is > 0; public async Task GetLengthAsync(string path, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path)) return null; var password = await _owner.RequirePasswordAsync(_settings); var info = await _owner._client.TryGetObjectAsync(_settings, password, OpenListTransferService.ToActualPath(_settings, path), cancellationToken); return info is { IsDirectory: false } ? info.Size : null; } public async Task WriteAsync(string path, Stream source, long? contentLength = null, string contentType = null, CancellationToken cancellationToken = default) { var current = await _owner._settingsService.GetAsync(); if (!string.Equals(StorageConfigurationFingerprint.Create(current), StorageConfigurationFingerprint.Create(_settings), StringComparison.Ordinal)) throw new InvalidOperationException("OpenList 配置已变化,绑定存储拒绝写入新目标。"); await _owner._transfers.TransferAsync(path, source, contentLength, cancellationToken); } public async Task OpenReadAsync(string path, long? from = null, long? to = null, CancellationToken cancellationToken = default) { var password = await _owner.RequirePasswordAsync(_settings); return await _owner._client.OpenReadAsync(_settings, password, OpenListTransferService.ToActualPath(_settings, path), from, to, cancellationToken); } public async Task DeleteAsync(string path, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path)) return; var password = await _owner.RequirePasswordAsync(_settings); await _owner._client.DeleteObjectAsync(_settings, password, OpenListTransferService.ToActualPath(_settings, path), cancellationToken); } } } }