276 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|