feat : File caching option
This commit is contained in:
@@ -9,8 +9,14 @@ interface User {
|
||||
|
||||
export interface UserPreferences {
|
||||
showThumbnails: boolean;
|
||||
cacheMode: "memory" | "file";
|
||||
}
|
||||
|
||||
const defaultPreferences: UserPreferences = {
|
||||
showThumbnails: true,
|
||||
cacheMode: "memory",
|
||||
};
|
||||
|
||||
export class PreferencesService {
|
||||
private static async getCurrentUser(): Promise<User> {
|
||||
const userCookie = cookies().get("stripUser");
|
||||
@@ -27,39 +33,46 @@ export class PreferencesService {
|
||||
}
|
||||
}
|
||||
|
||||
static async getPreferences(): Promise<UserPreferences> {
|
||||
await connectDB();
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
const preferences = await PreferencesModel.findOne({ userId: user.id });
|
||||
if (!preferences) {
|
||||
// Créer les préférences par défaut si elles n'existent pas
|
||||
const defaultPreferences = await PreferencesModel.create({
|
||||
userId: user.id,
|
||||
showThumbnails: true,
|
||||
});
|
||||
static async getPreferences(userId: string): Promise<UserPreferences> {
|
||||
try {
|
||||
const preferences = await PreferencesModel.findOne({ userId });
|
||||
if (!preferences) {
|
||||
return {
|
||||
showThumbnails: true,
|
||||
cacheMode: "memory",
|
||||
};
|
||||
}
|
||||
return {
|
||||
showThumbnails: defaultPreferences.showThumbnails,
|
||||
showThumbnails: preferences.showThumbnails,
|
||||
cacheMode: preferences.cacheMode || "memory",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting preferences:", error);
|
||||
return {
|
||||
showThumbnails: true,
|
||||
cacheMode: "memory",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
showThumbnails: preferences.showThumbnails,
|
||||
};
|
||||
}
|
||||
|
||||
static async updatePreferences(preferences: Partial<UserPreferences>): Promise<UserPreferences> {
|
||||
await connectDB();
|
||||
const user = await this.getCurrentUser();
|
||||
static async updatePreferences(
|
||||
userId: string,
|
||||
preferences: Partial<UserPreferences>
|
||||
): Promise<UserPreferences> {
|
||||
try {
|
||||
const updatedPreferences = await PreferencesModel.findOneAndUpdate(
|
||||
{ userId },
|
||||
{ $set: preferences },
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
|
||||
const updatedPreferences = await PreferencesModel.findOneAndUpdate(
|
||||
{ userId: user.id },
|
||||
{ $set: preferences },
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
|
||||
return {
|
||||
showThumbnails: updatedPreferences.showThumbnails,
|
||||
};
|
||||
return {
|
||||
showThumbnails: updatedPreferences.showThumbnails,
|
||||
cacheMode: updatedPreferences.cacheMode || "memory",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating preferences:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
type CacheMode = "file" | "memory";
|
||||
|
||||
interface CacheConfig {
|
||||
mode: CacheMode;
|
||||
}
|
||||
|
||||
class ServerCacheService {
|
||||
private static instance: ServerCacheService;
|
||||
private cache: Map<string, { data: unknown; expiry: number }> = new Map();
|
||||
private cacheDir: string;
|
||||
private memoryCache: Map<string, { data: unknown; expiry: number }> = new Map();
|
||||
private config: CacheConfig = {
|
||||
mode: "memory",
|
||||
};
|
||||
|
||||
// Configuration des temps de cache en millisecondes
|
||||
private static readonly fiveMinutes = 5 * 60 * 1000;
|
||||
@@ -21,7 +34,83 @@ class ServerCacheService {
|
||||
};
|
||||
|
||||
private constructor() {
|
||||
// Private constructor to prevent external instantiation
|
||||
this.cacheDir = path.join(process.cwd(), ".cache");
|
||||
this.ensureCacheDirectory();
|
||||
this.cleanExpiredCache();
|
||||
}
|
||||
|
||||
private ensureCacheDirectory(): void {
|
||||
if (!fs.existsSync(this.cacheDir)) {
|
||||
fs.mkdirSync(this.cacheDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private getCacheFilePath(key: string): string {
|
||||
// Nettoyer la clé des caractères spéciaux et des doubles slashes
|
||||
const sanitizedKey = key.replace(/[<>:"|?*]/g, "_").replace(/\/+/g, "/");
|
||||
|
||||
const filePath = path.join(this.cacheDir, `${sanitizedKey}.json`);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private cleanExpiredCache(): void {
|
||||
if (!fs.existsSync(this.cacheDir)) return;
|
||||
|
||||
const cleanDirectory = (dirPath: string): boolean => {
|
||||
if (!fs.existsSync(dirPath)) return true;
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
let isEmpty = true;
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const isSubDirEmpty = cleanDirectory(itemPath);
|
||||
if (isSubDirEmpty) {
|
||||
try {
|
||||
fs.rmdirSync(itemPath);
|
||||
} catch (_error) {
|
||||
isEmpty = false;
|
||||
}
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
} else if (stats.isFile() && item.endsWith(".json")) {
|
||||
try {
|
||||
const content = fs.readFileSync(itemPath, "utf-8");
|
||||
const cached = JSON.parse(content);
|
||||
if (cached.expiry < Date.now()) {
|
||||
fs.unlinkSync(itemPath);
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
} catch (_error) {
|
||||
// Si le fichier est corrompu, on le supprime
|
||||
try {
|
||||
fs.unlinkSync(itemPath);
|
||||
} catch (_) {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
} catch (_error) {
|
||||
// En cas d'erreur sur le fichier/dossier, on continue
|
||||
isEmpty = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return isEmpty;
|
||||
};
|
||||
|
||||
cleanDirectory(this.cacheDir);
|
||||
}
|
||||
|
||||
public static getInstance(): ServerCacheService {
|
||||
@@ -39,44 +128,203 @@ class ServerCacheService {
|
||||
return ServerCacheService.DEFAULT_TTL[type];
|
||||
}
|
||||
|
||||
public setCacheMode(mode: CacheMode): void {
|
||||
if (this.config.mode === mode) return;
|
||||
|
||||
// Si on passe de mémoire à fichier, on sauvegarde le cache en mémoire
|
||||
if (mode === "file" && this.config.mode === "memory") {
|
||||
this.memoryCache.forEach((value, key) => {
|
||||
if (value.expiry > Date.now()) {
|
||||
this.saveToFile(key, value);
|
||||
}
|
||||
});
|
||||
this.memoryCache.clear();
|
||||
}
|
||||
// Si on passe de fichier à mémoire, on charge le cache fichier en mémoire
|
||||
else if (mode === "memory" && this.config.mode === "file") {
|
||||
this.loadFileCacheToMemory();
|
||||
}
|
||||
|
||||
this.config.mode = mode;
|
||||
console.log(`Cache mode switched to: ${mode}`);
|
||||
}
|
||||
|
||||
public getCacheMode(): CacheMode {
|
||||
return this.config.mode;
|
||||
}
|
||||
|
||||
private loadFileCacheToMemory(): void {
|
||||
if (!fs.existsSync(this.cacheDir)) return;
|
||||
|
||||
const loadDirectory = (dirPath: string) => {
|
||||
const items = fs.readdirSync(dirPath);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
try {
|
||||
const stats = fs.statSync(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
loadDirectory(itemPath);
|
||||
} else if (stats.isFile() && item.endsWith(".json")) {
|
||||
try {
|
||||
const content = fs.readFileSync(itemPath, "utf-8");
|
||||
const cached = JSON.parse(content);
|
||||
if (cached.expiry > Date.now()) {
|
||||
const key = path.relative(this.cacheDir, itemPath).slice(0, -5); // Remove .json
|
||||
this.memoryCache.set(key, cached);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore les fichiers corrompus
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore les erreurs d'accès
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadDirectory(this.cacheDir);
|
||||
}
|
||||
|
||||
private saveToFile(key: string, value: { data: unknown; expiry: number }): void {
|
||||
const filePath = this.getCacheFilePath(key);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, JSON.stringify(value), "utf-8");
|
||||
} catch (_error) {
|
||||
// Ignore les erreurs d'écriture
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met en cache des données avec une durée de vie
|
||||
*/
|
||||
set(key: string, data: any, type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"): void {
|
||||
this.cache.set(key, {
|
||||
const cacheData = {
|
||||
data,
|
||||
expiry: Date.now() + this.getTTL(type),
|
||||
});
|
||||
};
|
||||
|
||||
if (this.config.mode === "memory") {
|
||||
this.memoryCache.set(key, cacheData);
|
||||
} else {
|
||||
const filePath = this.getCacheFilePath(key);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, JSON.stringify(cacheData), "utf-8");
|
||||
} catch (error) {
|
||||
console.error(`Error writing cache file ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère des données du cache si elles sont valides
|
||||
*/
|
||||
get(key: string): any | null {
|
||||
const cached = this.cache.get(key);
|
||||
if (!cached) return null;
|
||||
if (this.config.mode === "memory") {
|
||||
const cached = this.memoryCache.get(key);
|
||||
if (!cached) return null;
|
||||
|
||||
const now = Date.now();
|
||||
if (cached.expiry > now) {
|
||||
return cached.data;
|
||||
if (cached.expiry > Date.now()) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
this.memoryCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
const filePath = this.getCacheFilePath(key);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const cached = JSON.parse(content);
|
||||
|
||||
if (cached.expiry > Date.now()) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
fs.unlinkSync(filePath);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Error reading cache file ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une entrée du cache
|
||||
*/
|
||||
delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
if (this.config.mode === "memory") {
|
||||
this.memoryCache.delete(key);
|
||||
} else {
|
||||
const filePath = this.getCacheFilePath(key);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide le cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
if (this.config.mode === "memory") {
|
||||
this.memoryCache.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.cacheDir)) return;
|
||||
|
||||
const removeDirectory = (dirPath: string) => {
|
||||
if (!fs.existsSync(dirPath)) return;
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
try {
|
||||
const stats = fs.statSync(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
removeDirectory(itemPath);
|
||||
try {
|
||||
fs.rmdirSync(itemPath);
|
||||
} catch (_error) {
|
||||
console.error(`Could not remove directory ${itemPath}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fs.unlinkSync(itemPath);
|
||||
} catch (_error) {
|
||||
console.error(`Could not remove file ${itemPath}`);
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
console.error(`Error accessing ${itemPath}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
removeDirectory(this.cacheDir);
|
||||
console.log("Cache cleared successfully");
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,19 +335,14 @@ class ServerCacheService {
|
||||
fetcher: () => Promise<T>,
|
||||
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
|
||||
): Promise<T> {
|
||||
const now = Date.now();
|
||||
const cached = this.cache.get(key);
|
||||
|
||||
if (cached && cached.expiry > now) {
|
||||
return cached.data as T;
|
||||
const cached = this.get(key);
|
||||
if (cached !== null) {
|
||||
return cached as T;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetcher();
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expiry: now + this.getTTL(type),
|
||||
});
|
||||
this.set(key, data, type);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -107,7 +350,7 @@ class ServerCacheService {
|
||||
}
|
||||
|
||||
invalidate(key: string): void {
|
||||
this.cache.delete(key);
|
||||
this.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user