Spaces:
Running
Running
File size: 13,170 Bytes
947c08e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
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; |