ComicMTL / frontend /constants /module /image_cache_storage.tsx
BloodyInside's picture
firsty
947c08e
raw
history blame
13.2 kB
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;