expo-duooomi-app/utils/aniStorage.ts

162 lines
4.5 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 { 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<ArrayBuffer> {
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<void> {
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<boolean> {
try {
const filePath = this.getFilePath(url)
const info = await FileSystem.getInfoAsync(filePath)
return info.exists
} 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)
throw Error(`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()