472 lines
26 KiB
C#
472 lines
26 KiB
C#
using System.Net;
|
||
using System.Net.Http.Headers;
|
||
using System.Text;
|
||
using System.Xml.Linq;
|
||
using dy.net.model.dto;
|
||
using dy.net.model.entity;
|
||
using dy.net.service;
|
||
|
||
namespace dy.net.storage
|
||
{
|
||
public class WebDavMediaStorage : IMediaStorage
|
||
{
|
||
private static readonly TimeSpan[] DefaultUploadVisibilityRetryDelays =
|
||
{
|
||
TimeSpan.Zero,
|
||
TimeSpan.FromMilliseconds(200),
|
||
TimeSpan.FromMilliseconds(400),
|
||
TimeSpan.FromMilliseconds(800),
|
||
TimeSpan.FromMilliseconds(1600),
|
||
TimeSpan.FromMilliseconds(3200)
|
||
};
|
||
|
||
private static readonly TimeSpan[] DefaultFinalVisibilityRetryDelays =
|
||
{
|
||
TimeSpan.Zero,
|
||
TimeSpan.FromMilliseconds(200),
|
||
TimeSpan.FromMilliseconds(400),
|
||
TimeSpan.FromMilliseconds(800),
|
||
TimeSpan.FromMilliseconds(1600),
|
||
TimeSpan.FromMilliseconds(3200),
|
||
TimeSpan.FromSeconds(5),
|
||
TimeSpan.FromSeconds(8),
|
||
TimeSpan.FromSeconds(12),
|
||
TimeSpan.FromSeconds(15)
|
||
};
|
||
|
||
private static readonly TimeSpan[] DirectoryConflictRetryDelays =
|
||
{
|
||
TimeSpan.Zero,
|
||
TimeSpan.FromMilliseconds(150),
|
||
TimeSpan.FromMilliseconds(400),
|
||
TimeSpan.FromMilliseconds(900),
|
||
TimeSpan.FromMilliseconds(2000),
|
||
TimeSpan.FromMilliseconds(4500),
|
||
TimeSpan.FromMilliseconds(8000)
|
||
};
|
||
|
||
private readonly IHttpClientFactory _clientFactory;
|
||
private readonly WebDavSettingsService _settingsService;
|
||
private readonly IReadOnlyList<TimeSpan> _uploadVisibilityRetryDelays;
|
||
private readonly IReadOnlyList<TimeSpan> _finalVisibilityRetryDelays;
|
||
|
||
public WebDavMediaStorage(IHttpClientFactory clientFactory, WebDavSettingsService settingsService)
|
||
: this(clientFactory, settingsService, DefaultUploadVisibilityRetryDelays, DefaultFinalVisibilityRetryDelays)
|
||
{
|
||
}
|
||
|
||
internal WebDavMediaStorage(
|
||
IHttpClientFactory clientFactory,
|
||
WebDavSettingsService settingsService,
|
||
IReadOnlyList<TimeSpan> uploadVisibilityRetryDelays,
|
||
IReadOnlyList<TimeSpan> finalVisibilityRetryDelays)
|
||
{
|
||
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
|
||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||
_uploadVisibilityRetryDelays = uploadVisibilityRetryDelays?.Count > 0
|
||
? uploadVisibilityRetryDelays
|
||
: throw new ArgumentException("上传校验重试间隔不能为空", nameof(uploadVisibilityRetryDelays));
|
||
_finalVisibilityRetryDelays = finalVisibilityRetryDelays?.Count > 0
|
||
? finalVisibilityRetryDelays
|
||
: throw new ArgumentException("MOVE 校验重试间隔不能为空", nameof(finalVisibilityRetryDelays));
|
||
}
|
||
|
||
public StorageType StorageType => StorageType.WebDav;
|
||
|
||
public async Task EnsureDirectoryAsync(string path, CancellationToken cancellationToken = default) =>
|
||
await EnsureDirectoryAsync(path, await _settingsService.GetAsync(), cancellationToken);
|
||
|
||
public async Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default) =>
|
||
!string.IsNullOrWhiteSpace(path) && await ExistsAsync(path, await _settingsService.GetAsync(), cancellationToken);
|
||
|
||
public async Task<long?> GetLengthAsync(string path, CancellationToken cancellationToken = default) =>
|
||
string.IsNullOrWhiteSpace(path) ? null : await GetLengthAsync(path, await _settingsService.GetAsync(), cancellationToken);
|
||
|
||
public async Task WriteAsync(string path, Stream source, long? contentLength = null, string contentType = null, CancellationToken cancellationToken = default) =>
|
||
await WriteAsync(path, source, contentLength, contentType, await _settingsService.GetAsync(), cancellationToken);
|
||
|
||
public async Task<StorageReadResult> OpenReadAsync(string path, long? from = null, long? to = null, CancellationToken cancellationToken = default) =>
|
||
await OpenReadAsync(path, from, to, await _settingsService.GetAsync(), cancellationToken);
|
||
|
||
public async Task DeleteAsync(string path, CancellationToken cancellationToken = default)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(path))
|
||
await DeleteAsync(path, await _settingsService.GetAsync(), cancellationToken);
|
||
}
|
||
|
||
public IMediaStorage Bind(WebDavSettings settings)
|
||
{
|
||
Validate(settings);
|
||
return new BoundWebDavMediaStorage(this, settings);
|
||
}
|
||
|
||
public async Task<(bool Success, string Message)> ProbeAsync(WebDavSettings settings, CancellationToken cancellationToken = default)
|
||
{
|
||
Validate(settings);
|
||
var probeDirectory = $"/.dysync-probe-{Guid.NewGuid():N}";
|
||
var sourcePath = StoragePath.CombineRemote(probeDirectory, "source.txt");
|
||
var movedPath = StoragePath.CombineRemote(probeDirectory, "moved.txt");
|
||
var bytes = Encoding.UTF8.GetBytes("dysync-webdav-probe");
|
||
|
||
try
|
||
{
|
||
await EnsureDirectoryAsync(probeDirectory, settings, cancellationToken);
|
||
await using (var source = new MemoryStream(bytes, false))
|
||
await WriteAsync(sourcePath, source, bytes.Length, "text/plain", settings, cancellationToken);
|
||
|
||
var length = await GetLengthAsync(sourcePath, settings, cancellationToken);
|
||
if (length != bytes.Length) return (false, $"上传长度校验失败,期望 {bytes.Length},实际 {length?.ToString() ?? "未知"}");
|
||
|
||
await MoveAsync(sourcePath, movedPath, settings, cancellationToken);
|
||
await using (var read = await OpenReadAsync(movedPath, 0, 0, settings, cancellationToken))
|
||
{
|
||
if (read.StatusCode != (int)HttpStatusCode.PartialContent
|
||
|| !ContentRangeHeaderValue.TryParse(read.ContentRange, out var contentRange)
|
||
|| !contentRange.HasRange
|
||
|| contentRange.Unit != "bytes"
|
||
|| contentRange.From != 0
|
||
|| contentRange.To != 0
|
||
|| !contentRange.HasLength
|
||
|| contentRange.Length < 1)
|
||
return (false, "服务不支持媒体播放所需的标准 Range 读取(需要 206 和有效 Content-Range)");
|
||
}
|
||
await DeleteAsync(movedPath, settings, cancellationToken);
|
||
if (await ExistsAsync(movedPath, settings, cancellationToken))
|
||
return (false, "服务未能删除能力探测文件");
|
||
|
||
return (true, "AList/OpenList WebDAV 读写、移动、Range 与删除能力正常");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Warning(ex, "WebDAV 能力探测失败");
|
||
return (false, ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
await TryDeleteAsync(sourcePath, settings, CancellationToken.None);
|
||
await TryDeleteAsync(movedPath, settings, CancellationToken.None);
|
||
await TryDeleteAsync(probeDirectory, settings, CancellationToken.None);
|
||
}
|
||
}
|
||
|
||
private async Task EnsureDirectoryAsync(string path, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
var combined = CombinedPath(settings, path);
|
||
var segments = combined.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||
var current = string.Empty;
|
||
|
||
foreach (var segment in segments)
|
||
{
|
||
current = StoragePath.CombineRemote(current, segment);
|
||
await EnsureDirectorySegmentAsync(current, settings, cancellationToken);
|
||
}
|
||
}
|
||
|
||
private async Task EnsureDirectorySegmentAsync(
|
||
string path,
|
||
WebDavSettings settings,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
HttpStatusCode lastStatus = 0;
|
||
foreach (var delay in DirectoryConflictRetryDelays)
|
||
{
|
||
if (delay > TimeSpan.Zero) await Task.Delay(delay, cancellationToken);
|
||
using var request = await CreateRequestAsync(new HttpMethod("MKCOL"), path, settings);
|
||
using var response = await SendAsync(
|
||
request,
|
||
settings,
|
||
HttpCompletionOption.ResponseContentRead,
|
||
cancellationToken);
|
||
lastStatus = response.StatusCode;
|
||
if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.MethodNotAllowed) return;
|
||
if (response.StatusCode != HttpStatusCode.Conflict) break;
|
||
|
||
// AList/OpenList and some mounted storage drivers may return 409 when
|
||
// another request has just created the same collection, or while a
|
||
// newly-created parent is becoming visible. Only reuse the path after
|
||
// PROPFIND confirms that it is a collection; otherwise retry briefly.
|
||
if (await DirectoryExistsAsync(path, settings, cancellationToken)) return;
|
||
}
|
||
|
||
throw new HttpRequestException($"创建 WebDAV 目录失败:{path} ({(int)lastStatus})");
|
||
}
|
||
|
||
private async Task<bool> DirectoryExistsAsync(
|
||
string combinedPath,
|
||
WebDavSettings settings,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
using var request = await CreateRequestAsync(new HttpMethod("PROPFIND"), combinedPath, settings);
|
||
request.Headers.TryAddWithoutValidation("Depth", "0");
|
||
request.Content = new StringContent(
|
||
"<?xml version=\"1.0\"?><propfind xmlns=\"DAV:\"><prop><resourcetype/></prop></propfind>",
|
||
Encoding.UTF8,
|
||
"application/xml");
|
||
using var response = await SendAsync(
|
||
request,
|
||
settings,
|
||
HttpCompletionOption.ResponseContentRead,
|
||
cancellationToken);
|
||
if (response.StatusCode == HttpStatusCode.NotFound) return false;
|
||
if (!response.IsSuccessStatusCode && (int)response.StatusCode != 207) return false;
|
||
|
||
try
|
||
{
|
||
var xml = XDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
|
||
return xml.Descendants().Any(x => x.Name.LocalName == "collection");
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private async Task<bool> ExistsAsync(string path, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
using var request = await CreateRequestAsync(new HttpMethod("PROPFIND"), CombinedPath(settings, path), settings);
|
||
request.Headers.TryAddWithoutValidation("Depth", "0");
|
||
using var response = await SendAsync(request, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
if (response.StatusCode == HttpStatusCode.NotFound) return false;
|
||
return response.IsSuccessStatusCode || (int)response.StatusCode == 207;
|
||
}
|
||
|
||
private async Task<long?> GetLengthAsync(string path, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
using (var head = await CreateRequestAsync(HttpMethod.Head, CombinedPath(settings, path), settings))
|
||
{
|
||
AddNoCacheHeaders(head);
|
||
using var response = await SendAsync(head, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
if (response.StatusCode == HttpStatusCode.NotFound) return null;
|
||
if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength.HasValue)
|
||
return response.Content.Headers.ContentLength.Value;
|
||
}
|
||
|
||
using var request = await CreateRequestAsync(new HttpMethod("PROPFIND"), CombinedPath(settings, path), settings);
|
||
AddNoCacheHeaders(request);
|
||
request.Headers.TryAddWithoutValidation("Depth", "0");
|
||
request.Content = new StringContent("<?xml version=\"1.0\"?><propfind xmlns=\"DAV:\"><prop><getcontentlength/></prop></propfind>", Encoding.UTF8, "application/xml");
|
||
using var propResponse = await SendAsync(request, settings, HttpCompletionOption.ResponseContentRead, cancellationToken);
|
||
if (propResponse.StatusCode == HttpStatusCode.NotFound) return null;
|
||
propResponse.EnsureSuccessStatusCode();
|
||
var xml = XDocument.Parse(await propResponse.Content.ReadAsStringAsync(cancellationToken));
|
||
var value = xml.Descendants().FirstOrDefault(x => x.Name.LocalName == "getcontentlength")?.Value;
|
||
return long.TryParse(value, out var length) ? length : null;
|
||
}
|
||
|
||
private async Task WriteAsync(string path, Stream source, long? contentLength, string contentType, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
path = StoragePath.NormalizeRemote(path);
|
||
if (path == "/") throw new ArgumentException("WebDAV 文件路径不能为空", nameof(path));
|
||
await EnsureDirectoryAsync(StoragePath.DirectoryName(path), settings, cancellationToken);
|
||
var temporaryPath = path + ".part-" + Guid.NewGuid().ToString("N");
|
||
|
||
try
|
||
{
|
||
using var put = await CreateRequestAsync(HttpMethod.Put, CombinedPath(settings, temporaryPath), settings);
|
||
put.Content = new StreamContent(source, 81920);
|
||
if (contentLength.HasValue) put.Content.Headers.ContentLength = contentLength;
|
||
if (!string.IsNullOrWhiteSpace(contentType)) put.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
||
using var response = await SendAsync(put, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
var actualLength = await WaitForReadableLengthAsync(
|
||
temporaryPath, contentLength, settings, _uploadVisibilityRetryDelays, cancellationToken);
|
||
if (contentLength.HasValue && actualLength != contentLength)
|
||
throw new IOException($"WebDAV 上传长度不一致:期望 {contentLength},实际 {actualLength?.ToString() ?? "未知"}");
|
||
if (!actualLength.HasValue || actualLength <= 0)
|
||
throw new IOException("WebDAV 上传后的临时文件不存在或为空");
|
||
|
||
await MoveAsync(temporaryPath, path, settings, cancellationToken);
|
||
var expectedFinalLength = contentLength ?? actualLength;
|
||
var finalLength = await WaitForReadableLengthAsync(
|
||
path, expectedFinalLength, settings, _finalVisibilityRetryDelays, cancellationToken);
|
||
if (!finalLength.HasValue || finalLength <= 0)
|
||
throw new IOException($"WebDAV MOVE 后等待约 {TotalDelaySeconds(_finalVisibilityRetryDelays):0.#} 秒,最终文件仍不存在或为空");
|
||
if (expectedFinalLength.HasValue && expectedFinalLength > 0 && finalLength != expectedFinalLength)
|
||
throw new IOException($"WebDAV MOVE 后等待约 {TotalDelaySeconds(_finalVisibilityRetryDelays):0.#} 秒,最终文件仍返回旧长度或长度不一致:期望 {expectedFinalLength},实际 {finalLength}");
|
||
}
|
||
finally
|
||
{
|
||
await TryDeleteAsync(temporaryPath, settings, CancellationToken.None);
|
||
}
|
||
}
|
||
|
||
private async Task MoveAsync(string sourcePath, string destinationPath, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
using var request = await CreateRequestAsync(new HttpMethod("MOVE"), CombinedPath(settings, sourcePath), settings);
|
||
request.Headers.TryAddWithoutValidation("Destination", BuildUri(settings, CombinedPath(settings, destinationPath)).AbsoluteUri);
|
||
request.Headers.TryAddWithoutValidation("Overwrite", "T");
|
||
using var response = await SendAsync(request, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
response.EnsureSuccessStatusCode();
|
||
}
|
||
|
||
private async Task<long?> WaitForReadableLengthAsync(
|
||
string path,
|
||
long? expectedLength,
|
||
WebDavSettings settings,
|
||
IReadOnlyList<TimeSpan> retryDelays,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
long? lastLength = null;
|
||
for (var attempt = 0; attempt < retryDelays.Count; attempt++)
|
||
{
|
||
var delay = retryDelays[attempt];
|
||
if (delay > TimeSpan.Zero) await Task.Delay(delay, cancellationToken);
|
||
|
||
lastLength = await GetLengthAsync(path, settings, cancellationToken);
|
||
if (IsUsableLength(lastLength, expectedLength)) return lastLength;
|
||
|
||
// Some AList/OpenList storage drivers acknowledge MOVE before their metadata
|
||
// cache can answer HEAD/PROPFIND. A one-byte ranged read checks the actual object
|
||
// without downloading the whole media and also yields its total length.
|
||
var readableLength = await TryGetLengthFromRangeAsync(path, settings, cancellationToken);
|
||
if (IsUsableLength(readableLength, expectedLength))
|
||
{
|
||
if (attempt > 0 || !lastLength.HasValue)
|
||
Serilog.Log.Debug("WebDAV 元数据延迟,已通过 Range 读取确认文件:{Path}", path);
|
||
return readableLength;
|
||
}
|
||
if (readableLength.HasValue) lastLength = readableLength;
|
||
}
|
||
return lastLength;
|
||
}
|
||
|
||
private async Task<long?> TryGetLengthFromRangeAsync(
|
||
string path,
|
||
WebDavSettings settings,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
using var request = await CreateRequestAsync(HttpMethod.Get, CombinedPath(settings, path), settings);
|
||
AddNoCacheHeaders(request);
|
||
request.Headers.Range = new RangeHeaderValue(0, 0);
|
||
using var response = await SendAsync(request, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
if (response.StatusCode == HttpStatusCode.NotFound) return null;
|
||
if (response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
|
||
return response.Content.Headers.ContentRange?.Length ?? 0;
|
||
response.EnsureSuccessStatusCode();
|
||
if (response.Content.Headers.ContentRange?.Length is long totalLength) return totalLength;
|
||
return response.StatusCode == HttpStatusCode.OK
|
||
? response.Content.Headers.ContentLength
|
||
: null;
|
||
}
|
||
|
||
private static bool IsUsableLength(long? actualLength, long? expectedLength) =>
|
||
actualLength > 0 && (!expectedLength.HasValue || expectedLength.Value <= 0 || actualLength == expectedLength);
|
||
|
||
private static void AddNoCacheHeaders(HttpRequestMessage request)
|
||
{
|
||
request.Headers.CacheControl = new CacheControlHeaderValue
|
||
{
|
||
NoCache = true,
|
||
NoStore = true,
|
||
MaxAge = TimeSpan.Zero
|
||
};
|
||
request.Headers.Pragma.ParseAdd("no-cache");
|
||
}
|
||
|
||
private static double TotalDelaySeconds(IEnumerable<TimeSpan> delays) =>
|
||
delays.Sum(delay => delay.TotalSeconds);
|
||
|
||
private async Task<StorageReadResult> OpenReadAsync(string path, long? from, long? to, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
var request = await CreateRequestAsync(HttpMethod.Get, CombinedPath(settings, path), settings);
|
||
if (from.HasValue) request.Headers.Range = new RangeHeaderValue(from, to);
|
||
var response = await SendAsync(request, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
request.Dispose();
|
||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||
{
|
||
response.Dispose();
|
||
throw new FileNotFoundException("WebDAV 媒体文件不存在", path);
|
||
}
|
||
response.EnsureSuccessStatusCode();
|
||
return new StorageReadResult
|
||
{
|
||
Stream = await response.Content.ReadAsStreamAsync(cancellationToken),
|
||
ContentLength = response.Content.Headers.ContentLength,
|
||
ContentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream",
|
||
ContentRange = response.Content.Headers.ContentRange?.ToString(),
|
||
StatusCode = (int)response.StatusCode,
|
||
Owner = new HttpResponseOwner(response)
|
||
};
|
||
}
|
||
|
||
private async Task DeleteAsync(string path, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
Validate(settings);
|
||
using var request = await CreateRequestAsync(HttpMethod.Delete, CombinedPath(settings, path), settings);
|
||
using var response = await SendAsync(request, settings, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||
if (response.StatusCode != HttpStatusCode.NotFound) response.EnsureSuccessStatusCode();
|
||
}
|
||
|
||
private async Task TryDeleteAsync(string path, WebDavSettings settings, CancellationToken cancellationToken)
|
||
{
|
||
try { await DeleteAsync(path, settings, cancellationToken); }
|
||
catch (Exception ex) { Serilog.Log.Debug(ex, "清理 WebDAV 临时路径失败:{Path}", path); }
|
||
}
|
||
|
||
private async Task<HttpRequestMessage> CreateRequestAsync(HttpMethod method, string path, WebDavSettings settings)
|
||
{
|
||
var request = new HttpRequestMessage(method, BuildUri(settings, path));
|
||
var password = await _settingsService.GetPasswordAsync(settings);
|
||
var token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.UserName}:{password}"));
|
||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", token);
|
||
return request;
|
||
}
|
||
|
||
private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, WebDavSettings settings, HttpCompletionOption option, CancellationToken cancellationToken)
|
||
{
|
||
var client = _clientFactory.CreateClient(settings.AllowInvalidCertificate ? "webdav-insecure" : "webdav");
|
||
return await client.SendAsync(request, option, cancellationToken);
|
||
}
|
||
|
||
private static string CombinedPath(WebDavSettings settings, string path) =>
|
||
StoragePath.CombineRemote(settings.BasePath, path);
|
||
|
||
private static Uri BuildUri(WebDavSettings settings, string combinedPath)
|
||
{
|
||
var endpoint = settings.Endpoint.TrimEnd('/');
|
||
return new Uri(endpoint + StoragePath.Encode(combinedPath), UriKind.Absolute);
|
||
}
|
||
|
||
private static void Validate(WebDavSettings settings)
|
||
{
|
||
if (settings == null) throw new InvalidOperationException("尚未配置 WebDAV");
|
||
if (!Uri.TryCreate(settings.Endpoint, UriKind.Absolute, out var endpoint) || (endpoint.Scheme != "http" && endpoint.Scheme != "https"))
|
||
throw new InvalidOperationException("WebDAV 地址必须是有效的 HTTP/HTTPS 地址,例如 http://host:5244/dav");
|
||
if (string.IsNullOrWhiteSpace(settings.UserName)) throw new InvalidOperationException("WebDAV 用户名不能为空");
|
||
StoragePath.NormalizeRemote(settings.BasePath);
|
||
}
|
||
|
||
private sealed class BoundWebDavMediaStorage : IMediaStorage
|
||
{
|
||
private readonly WebDavMediaStorage _owner;
|
||
private readonly WebDavSettings _settings;
|
||
|
||
public BoundWebDavMediaStorage(WebDavMediaStorage owner, WebDavSettings settings)
|
||
{
|
||
_owner = owner;
|
||
_settings = settings;
|
||
}
|
||
|
||
public StorageType StorageType => StorageType.WebDav;
|
||
public Task EnsureDirectoryAsync(string path, CancellationToken cancellationToken = default) =>
|
||
_owner.EnsureDirectoryAsync(path, _settings, cancellationToken);
|
||
public Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default) =>
|
||
string.IsNullOrWhiteSpace(path) ? Task.FromResult(false) : _owner.ExistsAsync(path, _settings, cancellationToken);
|
||
public Task<long?> GetLengthAsync(string path, CancellationToken cancellationToken = default) =>
|
||
string.IsNullOrWhiteSpace(path) ? Task.FromResult<long?>(null) : _owner.GetLengthAsync(path, _settings, cancellationToken);
|
||
public Task WriteAsync(string path, Stream source, long? contentLength = null, string contentType = null, CancellationToken cancellationToken = default) =>
|
||
_owner.WriteAsync(path, source, contentLength, contentType, _settings, cancellationToken);
|
||
public Task<StorageReadResult> OpenReadAsync(string path, long? from = null, long? to = null, CancellationToken cancellationToken = default) =>
|
||
_owner.OpenReadAsync(path, from, to, _settings, cancellationToken);
|
||
public Task DeleteAsync(string path, CancellationToken cancellationToken = default) =>
|
||
string.IsNullOrWhiteSpace(path) ? Task.CompletedTask : _owner.DeleteAsync(path, _settings, cancellationToken);
|
||
}
|
||
}
|
||
}
|