feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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<bool> ExistsAsync(string path, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(File.Exists(path));
|
||||
|
||||
public Task<long?> GetLengthAsync(string path, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult<long?>(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<StorageReadResult> 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user