202 lines
6.5 KiB
C#
202 lines
6.5 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using HtmlToPdfService.Core.Options;
|
||
|
||
namespace HtmlToPdfService.Core.Storage;
|
||
|
||
/// <summary>
|
||
/// 本地文件存储实现
|
||
/// </summary>
|
||
public class LocalFileStorage : IFileStorage
|
||
{
|
||
private readonly StorageOptions _options;
|
||
private readonly ILogger<LocalFileStorage> _logger;
|
||
|
||
public LocalFileStorage(IOptions<PdfServiceOptions> options, ILogger<LocalFileStorage> logger)
|
||
{
|
||
_options = options.Value.Storage;
|
||
_logger = logger;
|
||
|
||
// 确保存储目录存在
|
||
if (!Directory.Exists(_options.LocalPath))
|
||
{
|
||
Directory.CreateDirectory(_options.LocalPath);
|
||
_logger.LogInformation("创建存储目录: {Path}", _options.LocalPath);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存 PDF 文件
|
||
/// </summary>
|
||
public async Task<(string FilePath, string DownloadUrl)> SaveAsync(
|
||
string requestId,
|
||
byte[] pdfData,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrEmpty(requestId))
|
||
throw new ArgumentException("请求 ID 不能为空", nameof(requestId));
|
||
|
||
if (pdfData == null || pdfData.Length == 0)
|
||
throw new ArgumentException("PDF 数据不能为空", nameof(pdfData));
|
||
|
||
try
|
||
{
|
||
// 按日期组织目录结构:/pdfs/yyyy-MM-dd/
|
||
var dateFolder = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||
var dailyPath = Path.Combine(_options.LocalPath, dateFolder);
|
||
|
||
if (!Directory.Exists(dailyPath))
|
||
{
|
||
Directory.CreateDirectory(dailyPath);
|
||
}
|
||
|
||
var fileName = $"{requestId}.pdf";
|
||
var filePath = Path.Combine(dailyPath, fileName);
|
||
|
||
// 保存文件
|
||
await File.WriteAllBytesAsync(filePath, pdfData, cancellationToken);
|
||
|
||
_logger.LogInformation("PDF 文件已保存: {FilePath}, 大小: {Size} bytes",
|
||
filePath, pdfData.Length);
|
||
|
||
// 构造下载 URL(相对路径)
|
||
var downloadUrl = $"/api/pdf/download/{requestId}";
|
||
|
||
return (filePath, downloadUrl);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "保存 PDF 文件失败: {RequestId}", requestId);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取文件
|
||
/// </summary>
|
||
public async Task<byte[]?> GetAsync(string requestId, CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrEmpty(requestId))
|
||
throw new ArgumentException("请求 ID 不能为空", nameof(requestId));
|
||
|
||
try
|
||
{
|
||
// 搜索最近几天的目录
|
||
var searchDays = Math.Max(1, _options.RetentionHours / 24 + 1);
|
||
for (int i = 0; i < searchDays; i++)
|
||
{
|
||
var date = DateTime.UtcNow.AddDays(-i).ToString("yyyy-MM-dd");
|
||
var filePath = Path.Combine(_options.LocalPath, date, $"{requestId}.pdf");
|
||
|
||
if (File.Exists(filePath))
|
||
{
|
||
_logger.LogDebug("找到 PDF 文件: {FilePath}", filePath);
|
||
return await File.ReadAllBytesAsync(filePath, cancellationToken);
|
||
}
|
||
}
|
||
|
||
_logger.LogWarning("未找到 PDF 文件: {RequestId}", requestId);
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "读取 PDF 文件失败: {RequestId}", requestId);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除文件
|
||
/// </summary>
|
||
public Task DeleteAsync(string requestId, CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrEmpty(requestId))
|
||
throw new ArgumentException("请求 ID 不能为空", nameof(requestId));
|
||
|
||
try
|
||
{
|
||
// 搜索并删除文件
|
||
var searchDays = Math.Max(1, _options.RetentionHours / 24 + 1);
|
||
for (int i = 0; i < searchDays; i++)
|
||
{
|
||
var date = DateTime.UtcNow.AddDays(-i).ToString("yyyy-MM-dd");
|
||
var filePath = Path.Combine(_options.LocalPath, date, $"{requestId}.pdf");
|
||
|
||
if (File.Exists(filePath))
|
||
{
|
||
File.Delete(filePath);
|
||
_logger.LogInformation("已删除 PDF 文件: {FilePath}", filePath);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "删除 PDF 文件失败: {RequestId}", requestId);
|
||
throw;
|
||
}
|
||
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理过期文件
|
||
/// </summary>
|
||
public Task CleanupExpiredFilesAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
if (!Directory.Exists(_options.LocalPath))
|
||
return Task.CompletedTask;
|
||
|
||
var expirationTime = DateTime.UtcNow.AddHours(-_options.RetentionHours);
|
||
var deletedCount = 0;
|
||
|
||
// 遍历所有日期目录
|
||
var directories = Directory.GetDirectories(_options.LocalPath);
|
||
foreach (var directory in directories)
|
||
{
|
||
if (cancellationToken.IsCancellationRequested)
|
||
break;
|
||
|
||
try
|
||
{
|
||
// 检查目录中的文件
|
||
var files = Directory.GetFiles(directory, "*.pdf");
|
||
foreach (var file in files)
|
||
{
|
||
var fileInfo = new FileInfo(file);
|
||
if (fileInfo.LastWriteTimeUtc < expirationTime)
|
||
{
|
||
File.Delete(file);
|
||
deletedCount++;
|
||
}
|
||
}
|
||
|
||
// 如果目录为空,删除目录
|
||
if (!Directory.EnumerateFileSystemEntries(directory).Any())
|
||
{
|
||
Directory.Delete(directory);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "清理目录失败: {Directory}", directory);
|
||
}
|
||
}
|
||
|
||
if (deletedCount > 0)
|
||
{
|
||
_logger.LogInformation("清理过期文件完成,删除了 {Count} 个文件", deletedCount);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "清理过期文件失败");
|
||
}
|
||
|
||
return Task.CompletedTask;
|
||
}
|
||
}
|
||
|