Greenlens/services/imageCacheService.ts

114 lines
3.7 KiB
TypeScript

import * as FileSystemLegacy from 'expo-file-system/legacy';
const getCacheDir = (): string => {
const baseDir = FileSystemLegacy.documentDirectory ?? FileSystemLegacy.cacheDirectory;
if (!baseDir) {
throw new Error('No writable file system directory is available for image caching.');
}
return `${baseDir}plant-images/`;
};
const ensureCacheDir = async (): Promise<string> => {
const cacheDir = getCacheDir();
const dirInfo = await FileSystemLegacy.getInfoAsync(cacheDir);
if (!dirInfo.exists) {
await FileSystemLegacy.makeDirectoryAsync(cacheDir, { intermediates: true });
}
return cacheDir;
};
const hashString = (value: string): string => {
// FNV-1a 32-bit hash for stable cache file names.
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(36);
};
const getDataUriExtension = (uri: string): string => {
const mimeMatch = uri.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,/i);
const mimeType = mimeMatch?.[1]?.toLowerCase() || '';
if (mimeType.includes('png')) return 'png';
if (mimeType.includes('webp')) return 'webp';
if (mimeType.includes('gif')) return 'gif';
if (mimeType.includes('heic')) return 'heic';
if (mimeType.includes('heif')) return 'heif';
return 'jpg';
};
const getUriExtension = (uri: string): string => {
const cleanPath = uri.split(/[?#]/)[0];
const extensionMatch = cleanPath.match(/\.([a-zA-Z0-9]+)$/);
return extensionMatch?.[1]?.toLowerCase() || 'jpg';
};
const getFileName = (uri: string): string => {
const extension = uri.startsWith('data:')
? getDataUriExtension(uri)
: getUriExtension(uri);
return `${hashString(uri)}.${extension}`;
};
export const ImageCacheService = {
/**
* Check if an image is already cached locally.
*/
isCached: async (uri: string): Promise<{ exists: boolean; localUri: string }> => {
const cacheDir = await ensureCacheDir();
const fileName = getFileName(uri);
const localUri = `${cacheDir}${fileName}`;
const info = await FileSystemLegacy.getInfoAsync(localUri);
return { exists: info.exists, localUri };
},
/**
* Cache an image (base64 data URI or remote URL) and return the local file path.
*/
cacheImage: async (uri: string): Promise<string> => {
const cacheDir = await ensureCacheDir();
const fileName = getFileName(uri);
const localUri = `${cacheDir}${fileName}`;
const info = await FileSystemLegacy.getInfoAsync(localUri);
const exists = info.exists;
if (exists) return localUri;
if (uri.startsWith('data:')) {
// Extract base64 content after the comma
const base64Data = uri.split(',')[1];
if (!base64Data) throw new Error('Invalid base64 data URI');
await FileSystemLegacy.writeAsStringAsync(localUri, base64Data, {
encoding: FileSystemLegacy.EncodingType.Base64,
});
} else if (/^(file:\/\/|content:\/\/)/i.test(uri)) {
await FileSystemLegacy.copyAsync({ from: uri, to: localUri });
} else {
// Remote URL - download it
const downloadResult = await FileSystemLegacy.downloadAsync(uri, localUri);
if (downloadResult.status !== 200) {
throw new Error(`Failed to download image: HTTP ${downloadResult.status}`);
}
}
return localUri;
},
/**
* Delete a cached image by its local path.
*/
deleteCachedImage: async (localUri: string): Promise<void> => {
try {
const info = await FileSystemLegacy.getInfoAsync(localUri);
if (info.exists) {
await FileSystemLegacy.deleteAsync(localUri, { idempotent: true });
}
} catch (e) {
console.error('Failed to delete cached image', e);
}
},
};