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.
 
 
 
 

410 lines
14 KiB

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; }
}
}