expo-duooomi-app/utils/aniStorage.ts

140 lines
3.8 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.

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()