You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
411 lines
14 KiB
411 lines
14 KiB
|
1 month ago
|
using System;
|
||
|
|
using System.Collections.Concurrent;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Security.Cryptography;
|
||
|
|
using System.Text;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using Avalonia.Media.Imaging;
|
||
|
|
using Microsoft.Extensions.Logging;
|
||
|
|
using SixLabors.ImageSharp;
|
||
|
|
using SixLabors.ImageSharp.Processing;
|
||
|
|
|
||
|
|
namespace AuroraDesk.Presentation.Services;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 缩略图缓存服务(类似 Windows 11 的 Thumbnail Cache)
|
||
|
|
/// 核心策略:
|
||
|
|
/// 1. 内存缓存:快速访问最近使用的缩略图
|
||
|
|
/// 2. 磁盘缓存:持久化存储,避免重复生成
|
||
|
|
/// 3. 按需生成:只在需要时生成缩略图
|
||
|
|
/// 4. 异步加载:不阻塞 UI 线程
|
||
|
|
/// </summary>
|
||
|
|
public class ThumbnailCacheService
|
||
|
|
{
|
||
|
|
private static ThumbnailCacheService? _instance;
|
||
|
|
private static readonly object _lockObject = new();
|
||
|
|
|
||
|
|
// 内存缓存(LRU 策略,最近使用的缩略图)
|
||
|
|
private readonly ConcurrentDictionary<string, CachedThumbnail> _memoryCache = new();
|
||
|
|
|
||
|
|
// 正在加载的任务字典,避免重复加载
|
||
|
|
private readonly ConcurrentDictionary<string, Task<Bitmap?>> _loadingTasks = new();
|
||
|
|
|
||
|
|
// 缩略图大小(与 UI 中的 184x184 一致)
|
||
|
|
private const int ThumbnailSize = 184;
|
||
|
|
|
||
|
|
// 内存缓存最大大小(最多缓存 1000 张缩略图)
|
||
|
|
private const int MaxMemoryCacheSize = 1000;
|
||
|
|
|
||
|
|
// 磁盘缓存目录
|
||
|
|
private readonly string _cacheDirectory;
|
||
|
|
|
||
|
|
// 缓存过期时间(7天)
|
||
|
|
private static readonly TimeSpan CacheExpiration = TimeSpan.FromDays(7);
|
||
|
|
|
||
|
|
private readonly ILogger<ThumbnailCacheService>? _logger;
|
||
|
|
|
||
|
|
private ThumbnailCacheService(ILogger<ThumbnailCacheService>? logger = null)
|
||
|
|
{
|
||
|
|
_logger = logger;
|
||
|
|
|
||
|
|
// 使用系统临时目录 + 应用名称作为缓存目录
|
||
|
|
var tempPath = Path.GetTempPath();
|
||
|
|
_cacheDirectory = Path.Combine(tempPath, "AuroraDesk", "ThumbnailCache");
|
||
|
|
|
||
|
|
// 确保缓存目录存在
|
||
|
|
if (!Directory.Exists(_cacheDirectory))
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(_cacheDirectory);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 启动后台清理任务(定期清理过期缓存)
|
||
|
|
_ = Task.Run(CleanupExpiredCacheAsync);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static ThumbnailCacheService Instance
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if (_instance == null)
|
||
|
|
{
|
||
|
|
lock (_lockObject)
|
||
|
|
{
|
||
|
|
if (_instance == null)
|
||
|
|
{
|
||
|
|
_instance = new ThumbnailCacheService();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return _instance;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 获取缩略图(异步,按需加载)
|
||
|
|
/// </summary>
|
||
|
|
public async Task<Bitmap?> GetThumbnailAsync(string filePath)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查内存缓存
|
||
|
|
if (_memoryCache.TryGetValue(filePath, out var cached) && cached.Thumbnail != null)
|
||
|
|
{
|
||
|
|
cached.LastAccessed = DateTime.UtcNow;
|
||
|
|
return cached.Thumbnail;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查是否正在加载
|
||
|
|
if (_loadingTasks.TryGetValue(filePath, out var loadingTask))
|
||
|
|
{
|
||
|
|
return await loadingTask;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 创建新的加载任务
|
||
|
|
var task = LoadThumbnailInternalAsync(filePath);
|
||
|
|
_loadingTasks.TryAdd(filePath, task);
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var result = await task;
|
||
|
|
|
||
|
|
// 添加到内存缓存
|
||
|
|
if (result != null)
|
||
|
|
{
|
||
|
|
AddToMemoryCache(filePath, result);
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_loadingTasks.TryRemove(filePath, out _);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 内部加载缩略图方法(检查磁盘缓存,如果没有则生成)
|
||
|
|
/// </summary>
|
||
|
|
private async Task<Bitmap?> LoadThumbnailInternalAsync(string filePath)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 1. 检查磁盘缓存
|
||
|
|
var cacheFilePath = GetCacheFilePath(filePath);
|
||
|
|
if (File.Exists(cacheFilePath))
|
||
|
|
{
|
||
|
|
var cacheFileInfo = new FileInfo(cacheFilePath);
|
||
|
|
var originalFileInfo = new FileInfo(filePath);
|
||
|
|
|
||
|
|
// 检查缓存是否过期(原文件修改时间更新或缓存文件过期)
|
||
|
|
if (cacheFileInfo.LastWriteTime >= originalFileInfo.LastWriteTime &&
|
||
|
|
DateTime.UtcNow - cacheFileInfo.LastWriteTimeUtc < CacheExpiration)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 从磁盘缓存加载
|
||
|
|
await using var cacheStream = File.OpenRead(cacheFilePath);
|
||
|
|
var bitmap = new Bitmap(cacheStream);
|
||
|
|
_logger?.LogDebug("从磁盘缓存加载缩略图: {FilePath}", filePath);
|
||
|
|
return bitmap;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogWarning(ex, "从磁盘缓存加载缩略图失败: {FilePath}", filePath);
|
||
|
|
// 删除损坏的缓存文件
|
||
|
|
try { File.Delete(cacheFilePath); } catch { }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 缓存过期,删除
|
||
|
|
try { File.Delete(cacheFilePath); } catch { }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 生成新的缩略图
|
||
|
|
return await GenerateThumbnailAsync(filePath);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogError(ex, "加载缩略图失败: {FilePath}", filePath);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 生成缩略图并保存到磁盘缓存
|
||
|
|
/// </summary>
|
||
|
|
private async Task<Bitmap?> GenerateThumbnailAsync(string filePath)
|
||
|
|
{
|
||
|
|
return await Task.Run(() =>
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 使用 ImageSharp 加载并生成缩略图
|
||
|
|
using var image = Image.Load(filePath);
|
||
|
|
|
||
|
|
// 计算缩略图尺寸(保持宽高比)
|
||
|
|
var sourceWidth = image.Width;
|
||
|
|
var sourceHeight = image.Height;
|
||
|
|
|
||
|
|
int thumbnailWidth, thumbnailHeight;
|
||
|
|
|
||
|
|
if (sourceWidth <= ThumbnailSize && sourceHeight <= ThumbnailSize)
|
||
|
|
{
|
||
|
|
// 如果原图小于缩略图尺寸,直接使用原图尺寸
|
||
|
|
thumbnailWidth = sourceWidth;
|
||
|
|
thumbnailHeight = sourceHeight;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 计算缩放比例,保持宽高比
|
||
|
|
var scale = Math.Min(
|
||
|
|
(double)ThumbnailSize / sourceWidth,
|
||
|
|
(double)ThumbnailSize / sourceHeight);
|
||
|
|
|
||
|
|
thumbnailWidth = (int)(sourceWidth * scale);
|
||
|
|
thumbnailHeight = (int)(sourceHeight * scale);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 生成缩略图
|
||
|
|
image.Mutate(x => x.Resize(new ResizeOptions
|
||
|
|
{
|
||
|
|
Size = new Size(thumbnailWidth, thumbnailHeight),
|
||
|
|
Mode = ResizeMode.Max, // 保持宽高比,确保不超过指定尺寸
|
||
|
|
Sampler = KnownResamplers.Lanczos3 // 高质量缩放算法
|
||
|
|
}));
|
||
|
|
|
||
|
|
// 将 ImageSharp 图像转换为 Avalonia Bitmap
|
||
|
|
using var memoryStream = new MemoryStream();
|
||
|
|
image.SaveAsPng(memoryStream);
|
||
|
|
memoryStream.Position = 0;
|
||
|
|
|
||
|
|
var bitmap = new Bitmap(memoryStream);
|
||
|
|
|
||
|
|
// 保存到磁盘缓存(异步,不阻塞)
|
||
|
|
_ = SaveToDiskCacheAsync(filePath, memoryStream);
|
||
|
|
|
||
|
|
return bitmap;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogError(ex, "生成缩略图失败: {FilePath}", filePath);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 保存缩略图到磁盘缓存
|
||
|
|
/// </summary>
|
||
|
|
private async Task SaveToDiskCacheAsync(string filePath, MemoryStream thumbnailStream)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var cacheFilePath = GetCacheFilePath(filePath);
|
||
|
|
var cacheDir = Path.GetDirectoryName(cacheFilePath);
|
||
|
|
|
||
|
|
if (cacheDir != null && !Directory.Exists(cacheDir))
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(cacheDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 复制流到文件
|
||
|
|
await using var fileStream = File.Create(cacheFilePath);
|
||
|
|
thumbnailStream.Position = 0;
|
||
|
|
await thumbnailStream.CopyToAsync(fileStream);
|
||
|
|
|
||
|
|
_logger?.LogDebug("缩略图已保存到磁盘缓存: {FilePath}", filePath);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogWarning(ex, "保存缩略图到磁盘缓存失败: {FilePath}", filePath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 获取缓存文件路径(基于文件路径的哈希值)
|
||
|
|
/// </summary>
|
||
|
|
private string GetCacheFilePath(string filePath)
|
||
|
|
{
|
||
|
|
// 使用文件路径的哈希值作为缓存文件名
|
||
|
|
var hash = ComputeHash(filePath);
|
||
|
|
var extension = Path.GetExtension(filePath).ToLowerInvariant();
|
||
|
|
var cacheFileName = $"{hash}{extension}";
|
||
|
|
|
||
|
|
// 使用子目录分散文件(避免单目录文件过多)
|
||
|
|
var subDir = hash.Substring(0, 2);
|
||
|
|
return Path.Combine(_cacheDirectory, subDir, cacheFileName);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 计算文件路径的哈希值
|
||
|
|
/// </summary>
|
||
|
|
private string ComputeHash(string filePath)
|
||
|
|
{
|
||
|
|
var bytes = Encoding.UTF8.GetBytes(filePath);
|
||
|
|
using var sha256 = SHA256.Create();
|
||
|
|
var hashBytes = sha256.ComputeHash(bytes);
|
||
|
|
return BitConverter.ToString(hashBytes).Replace("-", "");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 添加到内存缓存(LRU 策略)
|
||
|
|
/// </summary>
|
||
|
|
private void AddToMemoryCache(string filePath, Bitmap thumbnail)
|
||
|
|
{
|
||
|
|
// 如果缓存已满,移除最久未使用的项
|
||
|
|
if (_memoryCache.Count >= MaxMemoryCacheSize)
|
||
|
|
{
|
||
|
|
var oldestKey = _memoryCache
|
||
|
|
.OrderBy(x => x.Value.LastAccessed)
|
||
|
|
.FirstOrDefault().Key;
|
||
|
|
|
||
|
|
if (oldestKey != null && _memoryCache.TryRemove(oldestKey, out var oldest))
|
||
|
|
{
|
||
|
|
oldest.Thumbnail?.Dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_memoryCache.TryAdd(filePath, new CachedThumbnail
|
||
|
|
{
|
||
|
|
Thumbnail = thumbnail,
|
||
|
|
LastAccessed = DateTime.UtcNow
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 清理过期缓存
|
||
|
|
/// </summary>
|
||
|
|
private async Task CleanupExpiredCacheAsync()
|
||
|
|
{
|
||
|
|
while (true)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await Task.Delay(TimeSpan.FromHours(1)); // 每小时清理一次
|
||
|
|
|
||
|
|
// 清理内存缓存中的过期项
|
||
|
|
var expiredKeys = _memoryCache
|
||
|
|
.Where(x => DateTime.UtcNow - x.Value.LastAccessed > TimeSpan.FromHours(1))
|
||
|
|
.Select(x => x.Key)
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
foreach (var key in expiredKeys)
|
||
|
|
{
|
||
|
|
if (_memoryCache.TryRemove(key, out var cached))
|
||
|
|
{
|
||
|
|
cached.Thumbnail?.Dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 清理磁盘缓存中的过期文件
|
||
|
|
if (Directory.Exists(_cacheDirectory))
|
||
|
|
{
|
||
|
|
var cutoffTime = DateTime.UtcNow - CacheExpiration;
|
||
|
|
foreach (var file in Directory.EnumerateFiles(_cacheDirectory, "*.*", SearchOption.AllDirectories))
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var fileInfo = new FileInfo(file);
|
||
|
|
if (fileInfo.LastWriteTimeUtc < cutoffTime)
|
||
|
|
{
|
||
|
|
File.Delete(file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
// 忽略删除失败的文件
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogError(ex, "清理缓存时出错");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 清除所有缓存
|
||
|
|
/// </summary>
|
||
|
|
public void ClearCache()
|
||
|
|
{
|
||
|
|
// 清除内存缓存
|
||
|
|
foreach (var cached in _memoryCache.Values)
|
||
|
|
{
|
||
|
|
cached.Thumbnail?.Dispose();
|
||
|
|
}
|
||
|
|
_memoryCache.Clear();
|
||
|
|
|
||
|
|
// 清除磁盘缓存
|
||
|
|
if (Directory.Exists(_cacheDirectory))
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
Directory.Delete(_cacheDirectory, true);
|
||
|
|
Directory.CreateDirectory(_cacheDirectory);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger?.LogError(ex, "清除磁盘缓存失败");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 缓存的缩略图项
|
||
|
|
/// </summary>
|
||
|
|
private class CachedThumbnail
|
||
|
|
{
|
||
|
|
public Bitmap? Thumbnail { get; set; }
|
||
|
|
public DateTime LastAccessed { get; set; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|