Files
douyin/storage/OpenListMediaStorage.cs
T

276 lines
14 KiB
C#

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<string> 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<bool> 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<long?> 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<StorageReadResult> 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<OpenListDirectoryListDto> 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);
}
/// <summary>
/// 验证登录、本地目录与 OpenList 源挂载映射、服务端复制、Range 读取和删除。
/// </summary>
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<string> 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<string> 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<bool> ExistsAsync(string path, CancellationToken cancellationToken = default) =>
await GetLengthAsync(path, cancellationToken) is > 0;
public async Task<long?> 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<StorageReadResult> 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);
}
}
}
}