Spaces:
Running
Running
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<IDBDatabase> { | |
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<IDBDatabase> { | |
if (!this.db) { | |
this.db = await this.initDB(); | |
} | |
return this.db; | |
} | |
// Store image data | |
static async store(link: string, data: Blob): Promise<void> { | |
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<Object | null> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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<void> { | |
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; |