AssetPro/electron/cache-manager.ts

452 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 通用缓存管理器
* 支持多种数据类型的本地缓存(JSON、文件、图片等)
* 开发环境存储在项目目录打包后存储在exe同级目录
*/
import { app } from 'electron'
import { promises as fs } from 'fs'
import path from 'path'
import crypto from 'crypto'
/**
* 缓存项元数据
*/
interface CacheMetadata {
key: string
type: 'json' | 'text' | 'binary' | 'image'
createdAt: number
updatedAt: number
size: number
ttl?: number // Time to live in milliseconds (可选)
}
/**
* 缓存配置
*/
interface CacheConfig {
maxAge?: number // 最大缓存时间(毫秒), 默认不过期
namespace?: string // 缓存命名空间,用于隔离不同模块的缓存
}
/**
* 缓存管理器类
*/
class CacheManager {
private cacheDir: string = ''
private metadataMap: Map<string, CacheMetadata> = new Map()
private initialized: boolean = false
/**
* 初始化缓存管理器
* 必须在 app.whenReady() 之后调用
*/
async init(): Promise<void> {
if (this.initialized) {
console.warn('CacheManager 已经初始化过了')
return
}
try {
// 判断是否为开发环境
const isDev = !app.isPackaged
if (isDev) {
// 开发环境: 存储在项目根目录的 .cache 文件夹
this.cacheDir = path.join(process.cwd(), '.cache')
} else {
// 生产环境: 存储在 exe 同级目录的 cache 文件夹
const exePath = process.execPath
const exeDir = path.dirname(exePath)
this.cacheDir = path.join(exeDir, 'cache')
}
// 确保缓存目录存在
await fs.mkdir(this.cacheDir, { recursive: true })
// 加载元数据
await this.loadMetadata()
this.initialized = true
console.log('CacheManager 初始化完成')
console.log('缓存目录:', this.cacheDir)
console.log('当前环境:', isDev ? '开发环境' : '生产环境')
} catch (error) {
console.error('CacheManager 初始化失败:', error)
throw error
}
}
/**
* 确保缓存管理器已初始化
*/
private ensureInitialized(): void {
if (!this.initialized) {
throw new Error('CacheManager 未初始化,请先调用 init()')
}
}
/**
* 生成缓存键的哈希值(用于文件名)
*/
private hashKey(key: string): string {
return crypto.createHash('md5').update(key).digest('hex')
}
/**
* 获取缓存文件路径
*/
private getCacheFilePath(key: string, namespace?: string): string {
const hash = this.hashKey(key)
if (namespace) {
const namespaceDir = path.join(this.cacheDir, namespace)
return path.join(namespaceDir, hash)
}
return path.join(this.cacheDir, hash)
}
/**
* 获取元数据文件路径
*/
private getMetadataPath(): string {
return path.join(this.cacheDir, '_metadata.json')
}
/**
* 加载元数据
*/
private async loadMetadata(): Promise<void> {
try {
const metadataPath = this.getMetadataPath()
const exists = await fs.access(metadataPath).then(() => true).catch(() => false)
if (exists) {
const content = await fs.readFile(metadataPath, 'utf-8')
const data = JSON.parse(content)
this.metadataMap = new Map(Object.entries(data))
console.log(`加载了 ${this.metadataMap.size} 条缓存元数据`)
}
} catch (error) {
console.warn('加载缓存元数据失败:', error)
this.metadataMap = new Map()
}
}
/**
* 保存元数据
*/
private async saveMetadata(): Promise<void> {
try {
const metadataPath = this.getMetadataPath()
const data = Object.fromEntries(this.metadataMap)
await fs.writeFile(metadataPath, JSON.stringify(data, null, 2), 'utf-8')
} catch (error) {
console.error('保存缓存元数据失败:', error)
}
}
/**
* 检查缓存是否过期
*/
private isExpired(metadata: CacheMetadata): boolean {
if (!metadata.ttl) return false
const now = Date.now()
return now - metadata.updatedAt > metadata.ttl
}
/**
* 设置缓存 (JSON数据)
*/
async setJSON<T = any>(key: string, data: T, config?: CacheConfig): Promise<void> {
this.ensureInitialized()
try {
const namespace = config?.namespace
const filePath = this.getCacheFilePath(key, namespace)
// 确保命名空间目录存在
if (namespace) {
const namespaceDir = path.join(this.cacheDir, namespace)
await fs.mkdir(namespaceDir, { recursive: true })
}
// 写入JSON数据
const jsonString = JSON.stringify(data, null, 2)
await fs.writeFile(filePath, jsonString, 'utf-8')
// 更新元数据
const stats = await fs.stat(filePath)
const metadata: CacheMetadata = {
key,
type: 'json',
createdAt: this.metadataMap.get(key)?.createdAt || Date.now(),
updatedAt: Date.now(),
size: stats.size,
ttl: config?.maxAge
}
this.metadataMap.set(key, metadata)
await this.saveMetadata()
console.log(`缓存已设置: ${key} (${stats.size} bytes)`)
} catch (error) {
console.error(`设置缓存失败 [${key}]:`, error)
throw error
}
}
/**
* 获取缓存 (JSON数据)
*/
async getJSON<T = any>(key: string, config?: CacheConfig): Promise<T | null> {
this.ensureInitialized()
try {
const metadata = this.metadataMap.get(key)
// 检查元数据是否存在
if (!metadata || metadata.type !== 'json') {
console.log(`缓存不存在或类型不匹配: ${key}`)
return null
}
// 检查是否过期
if (this.isExpired(metadata)) {
console.log(`缓存已过期: ${key}`)
await this.delete(key, config?.namespace)
return null
}
// 读取缓存文件
const namespace = config?.namespace
const filePath = this.getCacheFilePath(key, namespace)
const content = await fs.readFile(filePath, 'utf-8')
const data = JSON.parse(content)
console.log(`读取缓存: ${key}`)
return data
} catch (error) {
console.error(`读取缓存失败 [${key}]:`, error)
return null
}
}
/**
* 设置缓存 (二进制数据 - 用于图片等)
*/
async setBinary(key: string, buffer: Buffer, type: 'binary' | 'image' = 'binary', config?: CacheConfig): Promise<void> {
this.ensureInitialized()
try {
const namespace = config?.namespace
const filePath = this.getCacheFilePath(key, namespace)
// 确保命名空间目录存在
if (namespace) {
const namespaceDir = path.join(this.cacheDir, namespace)
await fs.mkdir(namespaceDir, { recursive: true })
}
// 写入二进制数据
await fs.writeFile(filePath, buffer)
// 更新元数据
const stats = await fs.stat(filePath)
const metadata: CacheMetadata = {
key,
type,
createdAt: this.metadataMap.get(key)?.createdAt || Date.now(),
updatedAt: Date.now(),
size: stats.size,
ttl: config?.maxAge
}
this.metadataMap.set(key, metadata)
await this.saveMetadata()
console.log(`二进制缓存已设置: ${key} (${stats.size} bytes)`)
} catch (error) {
console.error(`设置二进制缓存失败 [${key}]:`, error)
throw error
}
}
/**
* 获取缓存 (二进制数据)
*/
async getBinary(key: string, config?: CacheConfig): Promise<Buffer | null> {
this.ensureInitialized()
try {
const metadata = this.metadataMap.get(key)
// 检查元数据是否存在
if (!metadata || (metadata.type !== 'binary' && metadata.type !== 'image')) {
console.log(`二进制缓存不存在或类型不匹配: ${key}`)
return null
}
// 检查是否过期
if (this.isExpired(metadata)) {
console.log(`二进制缓存已过期: ${key}`)
await this.delete(key, config?.namespace)
return null
}
// 读取缓存文件
const namespace = config?.namespace
const filePath = this.getCacheFilePath(key, namespace)
const buffer = await fs.readFile(filePath)
console.log(`读取二进制缓存: ${key}`)
return buffer
} catch (error) {
console.error(`读取二进制缓存失败 [${key}]:`, error)
return null
}
}
/**
* 检查缓存是否存在
*/
async has(key: string, config?: CacheConfig): Promise<boolean> {
this.ensureInitialized()
try {
const metadata = this.metadataMap.get(key)
if (!metadata) return false
// 检查是否过期
if (this.isExpired(metadata)) {
await this.delete(key, config?.namespace)
return false
}
// 检查文件是否真实存在
const namespace = config?.namespace
const filePath = this.getCacheFilePath(key, namespace)
const exists = await fs.access(filePath).then(() => true).catch(() => false)
return exists
} catch (error) {
console.error(`检查缓存失败 [${key}]:`, error)
return false
}
}
/**
* 删除单个缓存
*/
async delete(key: string, namespace?: string): Promise<void> {
this.ensureInitialized()
try {
const filePath = this.getCacheFilePath(key, namespace)
// 删除缓存文件
await fs.unlink(filePath).catch(() => {})
// 删除元数据
this.metadataMap.delete(key)
await this.saveMetadata()
console.log(`缓存已删除: ${key}`)
} catch (error) {
console.error(`删除缓存失败 [${key}]:`, error)
}
}
/**
* 清空指定命名空间的所有缓存
*/
async clearNamespace(namespace: string): Promise<void> {
this.ensureInitialized()
try {
const namespaceDir = path.join(this.cacheDir, namespace)
// 删除命名空间目录
await fs.rm(namespaceDir, { recursive: true, force: true })
// 删除相关元数据
const keysToDelete: string[] = []
this.metadataMap.forEach((metadata, key) => {
const filePath = this.getCacheFilePath(key, namespace)
if (filePath.startsWith(namespaceDir)) {
keysToDelete.push(key)
}
})
keysToDelete.forEach(key => this.metadataMap.delete(key))
await this.saveMetadata()
console.log(`命名空间缓存已清空: ${namespace} (${keysToDelete.length} 项)`)
} catch (error) {
console.error(`清空命名空间缓存失败 [${namespace}]:`, error)
}
}
/**
* 清空所有缓存
*/
async clearAll(): Promise<void> {
this.ensureInitialized()
try {
// 删除所有缓存文件(保留元数据文件)
const files = await fs.readdir(this.cacheDir, { withFileTypes: true })
for (const file of files) {
const filePath = path.join(this.cacheDir, file.name)
// 跳过元数据文件
if (file.name === '_metadata.json') continue
if (file.isDirectory()) {
await fs.rm(filePath, { recursive: true, force: true })
} else {
await fs.unlink(filePath)
}
}
// 清空元数据
this.metadataMap.clear()
await this.saveMetadata()
console.log('所有缓存已清空')
} catch (error) {
console.error('清空所有缓存失败:', error)
}
}
/**
* 获取缓存统计信息
*/
async getStats(): Promise<{
totalItems: number
totalSize: number
cacheDir: string
}> {
this.ensureInitialized()
let totalSize = 0
this.metadataMap.forEach(metadata => {
totalSize += metadata.size
})
return {
totalItems: this.metadataMap.size,
totalSize,
cacheDir: this.cacheDir
}
}
/**
* 获取所有缓存键
*/
async getAllKeys(): Promise<string[]> {
this.ensureInitialized()
return Array.from(this.metadataMap.keys())
}
}
// 导出单例
export const cacheManager = new CacheManager()