import axios from 'axios'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc';dayjs.extend(utc); import { Platform } from 'react-native'; import * as SQLite from 'expo-sqlite'; import * as FileSystem from 'expo-file-system'; import {blobToBase64} from '@/constants/module/file_manager'; import Storage from '@/constants/module/storage'; // Most of the code here is generated by A.I with some edit and tweak. // I'm lazy to sit and implement entire thing :) // If you see any delulu, let me know. const DATABASE_NAME = 'ImageCacheDB'; const MAX_ROW = 50; const MAX_AGE = 3; // in days class ImageStorage_Web { private static db: IDBDatabase; // Initialize the database private static async initDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DATABASE_NAME, 1); request.onupgradeneeded = (event: IDBVersionChangeEvent) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains('images')) { db.createObjectStore('images', { keyPath: 'link' }); } }; request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.log('Error initializing database:', request.error); reject(request.error); }; }); } // Get the database instance private static async getDB(): Promise { if (!this.db) { this.db = await this.initDB(); } return this.db; } // Store image data static async store(link: string, data: Blob): Promise { const db = await this.getDB(); const transaction = db.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const timestamp = dayjs().unix(); store.put({ link, data, timestamp }); transaction.onerror = () => { console.log('Error storing image:', transaction.error); }; } // Get image data static async get(setShowCloudflareTurnstile:any, link: string, signal: AbortSignal): Promise { const db = await this.getDB(); const transaction = db.transaction('images', 'readonly'); const store = transaction.objectStore('images'); return new Promise(async (resolve, reject) => { const request = store.get(link); request.onsuccess = async () => { const result = request.result; if (result) { resolve({type:"blob",data:result.data}); } else { const countRequest = store.count(); countRequest.onsuccess = async () => { if (countRequest.result >= MAX_ROW) { await this.removeOldest(); } try { axios.get(link, { responseType: 'blob', timeout: 60000, signal: signal, headers: { 'X-CLOUDFLARE-TURNSTILE-TOKEN': await Storage.get("cloudflare-turnstile-token") }, }).then(async (response) => { const data = response.data; await this.store(link, data); await this.removeOldImages() resolve({type:"blob",data:data}); }).catch((error) => { if (error.status === 511) setShowCloudflareTurnstile(true) console.log(error) resolve({type:"error",data:error}) }); } catch (error) { console.log('Error fetching image:', error); resolve({type:"error",data:error}) } }; countRequest.onerror = () => { console.log('Error counting images:', countRequest.error); reject(countRequest.error); }; } }; request.onerror = () => { console.log('Error getting image:', request.error); reject(request.error); }; }); } // Remove image data static async remove(link: string): Promise { const db = await this.getDB(); const transaction = db.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); store.delete(link); transaction.onerror = () => { console.log('Error removing image:', transaction.error); }; } // Remove the oldest image data private static async removeOldest(): Promise { const db = await this.getDB(); const transaction = db.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (cursor) { store.delete(cursor.key); cursor.continue(); } }; transaction.onerror = () => { console.log('Error removing oldest image:', transaction.error); }; } // Remove images older than MAX_AGE days static async removeOldImages(): Promise { const db = await this.getDB(); const transaction = db.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (cursor) { const record = cursor.value; const age = dayjs().diff(dayjs.unix(record.timestamp), 'day'); if (age > MAX_AGE) { store.delete(cursor.key); } cursor.continue(); } }; transaction.onerror = () => { console.log('Error removing old images:', transaction.error); }; } } class ImageStorage_Native { private DATABASE:any constructor() { this.DATABASE = new Promise(async (resolve, reject) => { const _DATABASE = await SQLite.openDatabaseAsync(DATABASE_NAME) await _DATABASE.runAsync(`CREATE TABLE IF NOT EXISTS images ( link TEXT PRIMARY KEY NOT NULL, file_path TEXT NOT NULL, timestamp INTEGER NOT NULL );`) resolve(_DATABASE) }) } public async store(link: string, file_path: string): Promise { const db = await this.DATABASE const timestamp = dayjs().unix(); await db.runAsync('INSERT OR REPLACE INTO images (link, file_path, timestamp) VALUES (?, ?, ?);',link, file_path, timestamp) } public async get(setShowCloudflareTurnstile:any,link: string, signal: AbortSignal) { return new Promise(async (resolve, reject) => { try{ const db = await this.DATABASE // Remove all unmatched image in sqlite and local const file_path_list = (await db.getAllAsync('SELECT file_path FROM images')).map((item:any) => item.file_path); const dir_path = FileSystem.cacheDirectory + 'ComicMTL/'+ 'cover/'; const dirInfo = await FileSystem.getInfoAsync(dir_path); if (!dirInfo.exists) await FileSystem.makeDirectoryAsync(dir_path, { intermediates: true }); const local_file_path_list = (await FileSystem.readDirectoryAsync(dir_path)).map(file => dir_path + file);; const result_list = local_file_path_list.filter(item => !file_path_list.includes(item)); for (const file_path of result_list) { try { // Delete the file await FileSystem.deleteAsync(file_path, { idempotent: true }); } catch (error) { resolve({type:"error",data:error}) console.log('#0 Error deleting file from cache:', error); } } // Check if image link exists in sqlite and filesystem const result = await db.getFirstAsync('SELECT * FROM images WHERE link = ?;',link) const local_exist = result ? (await FileSystem.getInfoAsync(result.file_path)).exists : false if (result && local_exist) { // Image link exists, return the data resolve({type:"file_path",data:result.file_path}) }else{ const result = await db.getFirstAsync('SELECT COUNT(*) as count FROM images;') if (result.count >= MAX_ROW) { const result =await db.getFirstAsync('SELECT * FROM images WHERE timestamp = (SELECT MIN(timestamp) FROM images);') if (result) { try { // Delete the file const filePath = result.file_path; await FileSystem.deleteAsync(filePath, { idempotent: true }); } catch (error) { resolve({type:"error",data:error}) console.log('#1 Error deleting file from cache:', error); } await db.runAsync('DELETE FROM images WHERE timestamp = (SELECT MIN(timestamp) FROM images);') } } axios.get(link, { responseType: 'blob', timeout: 60000, signal: signal, headers: { 'X-CLOUDFLARE-TURNSTILE-TOKEN': await Storage.get("cloudflare-turnstile-token") }, }).then(async (response) => { const filename = response.headers['content-disposition'].match(/filename="([^"]+)"/)[1] const base64:string = await blobToBase64(response.data); const dir_path = FileSystem.cacheDirectory + "ComicMTL/" + "cover/" const dirInfo = await FileSystem.getInfoAsync(dir_path); if (!dirInfo.exists) { await FileSystem.makeDirectoryAsync(dir_path, { intermediates: true }); } const file_path = dir_path + filename; await FileSystem.writeAsStringAsync(file_path, base64.split(',')[1], { encoding: FileSystem.EncodingType.Base64, }); await this.store(link, file_path); // DELETE images older than MAX_AGE days const thresholdDate = dayjs().subtract(MAX_AGE * 24 * 60 * 60, 'second').unix(); const rows = await db.getAllAsync('SELECT * FROM images WHERE timestamp < ?;',thresholdDate); for (const row of rows) { const row_file_path = row.file_path; try { // Delete the file await FileSystem.deleteAsync(row_file_path, { idempotent: true }); await db.runAsync('DELETE FROM images WHERE link = ?;',row.link); } catch (error) { resolve({type:"error",data:error}) console.log('#2 Error deleting file from cache:', error); } } resolve({type:"file_path",data:file_path}) }).catch((error) => { resolve({type:"error",data:error}) if (error.status === 511) setShowCloudflareTurnstile(true) console.log(error) }); } }catch(error){ resolve({type:"error",data:error}) console.log(error) } }) } } var ImageCacheStorage:any if (Platform.OS === "web"){ ImageCacheStorage = ImageStorage_Web }else{ ImageCacheStorage = new ImageStorage_Native() } export default ImageCacheStorage;