import { decode as base64Decode, encode as base64Encode } from 'base-64' import * as FileSystem from 'expo-file-system/legacy' /** * 基于 Expo FileSystem 的持久化缓存系统,用于存储 .ani 文件数据 * 键是 URL,值是 ArrayBuffer 二进制数据 */ 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}` } /** * 将 ArrayBuffer 转换为 Base64 字符串 */ const arrayBufferToBase64 = (buffer: ArrayBuffer): string => { const bytes = new Uint8Array(buffer) let binary = '' for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]) } return base64Encode(binary) } /** * 将 Base64 字符串转换为 ArrayBuffer */ const base64ToArrayBuffer = (base64: string): ArrayBuffer => { const binary = base64Decode(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i) } return bytes.buffer } 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}.ani` } /** * 获取缓存中的 ani 数据 * @param url 资源的 URL * @returns 缓存的 ArrayBuffer 数据,如果不存在则返回 undefined */ async get(url: string): Promise { try { const filePath = this.getFilePath(url) // 使用 base64 编码读取二进制文件 const base64Content = await FileSystem.readAsStringAsync(filePath, { encoding: FileSystem.EncodingType.Base64, }) return base64ToArrayBuffer(base64Content) } catch (error) { // 文件不存在或读取失败时返回 undefined throw Error(`AniCache get error: ${error}`) } } /** * 设置 ani 数据到缓存中 * @param url 资源的 URL * @param data ArrayBuffer 数据 */ async set(url: string, data: ArrayBuffer): Promise { try { await this.ensureDir() const filePath = this.getFilePath(url) const base64Content = arrayBufferToBase64(data) await FileSystem.writeAsStringAsync(filePath, base64Content, { encoding: FileSystem.EncodingType.Base64, }) } catch (error) { console.warn('AniCache set error:', error) throw Error(`AniCache set error: ${error}`) } } /** * 检查缓存中是否存在该 URL (异步) * @param url 资源的 URL */ async has(url: string): Promise { try { const filePath = this.getFilePath(url) const info = await FileSystem.getInfoAsync(filePath) return info.exists } catch { return false } } /** * 清除所有缓存 */ async clear(): Promise { try { // idempotent: true 使得如果文件/目录不存在也不会报错 await FileSystem.deleteAsync(CACHE_FOLDER, { idempotent: true }) await this.ensureDir() console.debug('AniCache cleared all cache') } catch (error) { console.warn('AniCache clear error:', error) throw Error(`AniCache clear error: ${error}`) } } /** * 删除指定 URL 的缓存 * @param url 资源的 URL */ async delete(url: string): Promise { 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()