140 lines
3.8 KiB
TypeScript
140 lines
3.8 KiB
TypeScript
import * as FileSystem from 'expo-file-system/legacy'
|
||
|
||
/**
|
||
* 基于 Expo FileSystem 的持久化缓存系统,用于存储 .ani 文件数据
|
||
* 键是 URL,值是 ani 文件内容
|
||
*/
|
||
|
||
// 定义 ani 文件内容的类型
|
||
type AniData = object | string
|
||
|
||
const CACHE_FOLDER = `${FileSystem.cacheDirectory}ani_cache/`
|
||
|
||
/**
|
||
* 将 URL 转换为合法的文件名
|
||
*
|
||
* 为什么需要这样做?
|
||
* 1. URL 中包含文件系统不允许的特殊字符(如 /, :, ?, & 等)。
|
||
* 2. URL 的长度可能超过操作系统对文件名的长度限制(通常为 255 字符)。
|
||
* 3. 简单的哈希算法可以将任意长度的 URL 转换为简短且唯一的标识符。
|
||
*/
|
||
const hashUrl = (url: string): string => {
|
||
let hash = 0
|
||
for (let i = 0; i < url.length; i++) {
|
||
const char = url.charCodeAt(i)
|
||
hash = (hash << 5) - hash + char
|
||
hash |= 0 // Convert to 32bit integer
|
||
}
|
||
// 使用 hex 字符串和长度作为后缀来减少碰撞概率
|
||
return `${hash.toString(16)}_${url.length}`
|
||
}
|
||
|
||
class AniStorage {
|
||
constructor() {
|
||
this.ensureDir()
|
||
}
|
||
|
||
/**
|
||
* 确保存储目录存在
|
||
*/
|
||
private async ensureDir() {
|
||
try {
|
||
// 直接尝试创建目录,intermediates: true 会处理父目录不存在的情况
|
||
// 如果目录已存在,通常不会报错,或者我们在 catch 中忽略
|
||
await FileSystem.makeDirectoryAsync(CACHE_FOLDER, { intermediates: true })
|
||
} catch (error) {
|
||
// 忽略目录已存在的错误
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取缓存文件路径
|
||
*/
|
||
private getFilePath(url: string): string {
|
||
const filename = hashUrl(url)
|
||
return `${CACHE_FOLDER}${filename}.json`
|
||
}
|
||
|
||
/**
|
||
* 获取缓存中的 ani 数据
|
||
* @param url 资源的 URL
|
||
* @returns 缓存的数据,如果不存在则返回 undefined
|
||
*/
|
||
async get(url: string): Promise<AniData | undefined> {
|
||
try {
|
||
const filePath = this.getFilePath(url)
|
||
// 移除 getInfoAsync 检查,直接尝试读取
|
||
// 如果文件不存在,readAsStringAsync 会抛出错误,我们在 catch 中处理
|
||
const content = await FileSystem.readAsStringAsync(filePath)
|
||
try {
|
||
return JSON.parse(content)
|
||
} catch {
|
||
return content
|
||
}
|
||
} catch (error) {
|
||
// 文件不存在或读取失败时返回 undefined
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置 ani 数据到缓存中
|
||
* @param url 资源的 URL
|
||
* @param data ani 文件数据
|
||
*/
|
||
async set(url: string, data: AniData): Promise<void> {
|
||
try {
|
||
await this.ensureDir()
|
||
const filePath = this.getFilePath(url)
|
||
const content = typeof data === 'string' ? data : JSON.stringify(data)
|
||
await FileSystem.writeAsStringAsync(filePath, content)
|
||
} catch (error) {
|
||
console.warn('AniCache set error:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查缓存中是否存在该 URL (异步)
|
||
* @param url 资源的 URL
|
||
*/
|
||
async has(url: string): Promise<boolean> {
|
||
try {
|
||
const filePath = this.getFilePath(url)
|
||
// 由于 getInfoAsync 被弃用,这里尝试读取文件来检查是否存在
|
||
await FileSystem.readAsStringAsync(filePath)
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除所有缓存
|
||
*/
|
||
async clear(): Promise<void> {
|
||
try {
|
||
// idempotent: true 使得如果文件/目录不存在也不会报错
|
||
await FileSystem.deleteAsync(CACHE_FOLDER, { idempotent: true })
|
||
await this.ensureDir()
|
||
} catch (error) {
|
||
console.warn('AniCache clear error:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除指定 URL 的缓存
|
||
* @param url 资源的 URL
|
||
*/
|
||
async delete(url: string): Promise<void> {
|
||
try {
|
||
const filePath = this.getFilePath(url)
|
||
await FileSystem.deleteAsync(filePath, { idempotent: true })
|
||
} catch (error) {
|
||
console.warn('AniCache delete error:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
export const aniStorage = new AniStorage()
|