using dy.net.model.dto; namespace dy.net.storage { public class LocalMediaStorage : IMediaStorage { public StorageType StorageType => StorageType.Local; public Task EnsureDirectoryAsync(string path, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(path)) Directory.CreateDirectory(path); return Task.CompletedTask; } public Task ExistsAsync(string path, CancellationToken cancellationToken = default) => Task.FromResult(File.Exists(path)); public Task GetLengthAsync(string path, CancellationToken cancellationToken = default) => Task.FromResult(File.Exists(path) ? new FileInfo(path).Length : null); public async Task WriteAsync(string path, Stream source, long? contentLength = null, string contentType = null, CancellationToken cancellationToken = default) { var directory = Path.GetDirectoryName(path); if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); var tempPath = path + ".part-" + Guid.NewGuid().ToString("N"); try { await using (var output = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, true)) { await source.CopyToAsync(output, 81920, cancellationToken); await output.FlushAsync(cancellationToken); } var actualLength = new FileInfo(tempPath).Length; if (actualLength <= 0) throw new IOException("本地写入后的临时文件为空"); if (contentLength.HasValue && actualLength != contentLength.Value) throw new IOException($"本地写入长度不一致:期望 {contentLength.Value},实际 {actualLength}"); File.Move(tempPath, path, true); var finalLength = new FileInfo(path).Length; if (finalLength != actualLength) throw new IOException($"本地最终文件长度不一致:期望 {actualLength},实际 {finalLength}"); } finally { if (File.Exists(tempPath)) File.Delete(tempPath); } } public Task OpenReadAsync(string path, long? from = null, long? to = null, CancellationToken cancellationToken = default) { if (!File.Exists(path)) throw new FileNotFoundException("媒体文件不存在", path); var file = new FileInfo(path); var start = Math.Max(0, from ?? 0); var end = Math.Min(file.Length - 1, to ?? file.Length - 1); var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, true); stream.Seek(start, SeekOrigin.Begin); return Task.FromResult(new StorageReadResult { Stream = stream, ContentLength = end - start + 1, ContentType = MimeType(path), ContentRange = from.HasValue ? $"bytes {start}-{end}/{file.Length}" : null, StatusCode = from.HasValue ? 206 : 200 }); } public Task DeleteAsync(string path, CancellationToken cancellationToken = default) { if (File.Exists(path)) File.Delete(path); return Task.CompletedTask; } private static string MimeType(string path) => Path.GetExtension(path).ToLowerInvariant() switch { ".mp4" => "video/mp4", ".jpg" or ".jpeg" => "image/jpeg", ".png" => "image/png", ".mp3" => "audio/mpeg", ".nfo" or ".xml" => "application/xml", _ => "application/octet-stream" }; } }