feat : File caching option
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -36,6 +36,8 @@ next-env.d.ts
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
23
src/app/api/komga/cache/mode/route.ts
vendored
Normal file
23
src/app/api/komga/cache/mode/route.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { serverCacheService } from "@/lib/services/server-cache.service";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ mode: serverCacheService.getCacheMode() });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { mode } = await request.json();
|
||||||
|
if (mode !== "file" && mode !== "memory") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid mode. Must be 'file' or 'memory'" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCacheService.setCacheMode(mode);
|
||||||
|
return NextResponse.json({ mode: serverCacheService.getCacheMode() });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/components/settings/CacheModeSwitch.tsx
Normal file
59
src/components/settings/CacheModeSwitch.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
|
|
||||||
|
export function CacheModeSwitch() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { preferences, updatePreferences } = usePreferences();
|
||||||
|
|
||||||
|
const handleToggle = async (checked: boolean) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Mettre à jour les préférences
|
||||||
|
await updatePreferences({ cacheMode: checked ? "memory" : "file" });
|
||||||
|
|
||||||
|
// Mettre à jour le mode de cache côté serveur
|
||||||
|
const res = await fetch("/api/komga/cache/mode", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ mode: checked ? "memory" : "file" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Mode de cache modifié",
|
||||||
|
description: `Le cache est maintenant en mode ${checked ? "mémoire" : "fichier"}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de modifier le mode de cache",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id="cache-mode"
|
||||||
|
checked={preferences.cacheMode === "memory"}
|
||||||
|
onCheckedChange={handleToggle}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="cache-mode" className="text-sm text-muted-foreground">
|
||||||
|
Cache en mémoire {isLoading && "(chargement...)"}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { useToast } from "@/components/ui/use-toast";
|
|||||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch";
|
||||||
|
|
||||||
interface KomgaConfig {
|
interface KomgaConfig {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -416,6 +417,16 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="cache-mode">Mode de cache</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Le cache en mémoire est plus rapide mais ne persiste pas entre les redémarrages
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CacheModeSwitch />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Formulaire TTL */}
|
{/* Formulaire TTL */}
|
||||||
<form onSubmit={handleSaveTTL} className="space-y-4">
|
<form onSubmit={handleSaveTTL} className="space-y-4">
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
|||||||
25
src/components/ui/separator.tsx
Normal file
25
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { UserPreferences } from "@/lib/services/preferences.service";
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
showThumbnails: boolean;
|
||||||
|
cacheMode: "memory" | "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPreferences: UserPreferences = {
|
||||||
|
showThumbnails: true,
|
||||||
|
cacheMode: "memory",
|
||||||
|
};
|
||||||
|
|
||||||
interface PreferencesContextType {
|
interface PreferencesContextType {
|
||||||
preferences: UserPreferences;
|
preferences: UserPreferences;
|
||||||
@@ -12,9 +21,7 @@ interface PreferencesContextType {
|
|||||||
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function PreferencesProvider({ children }: { children: React.ReactNode }) {
|
export function PreferencesProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [preferences, setPreferences] = useState<UserPreferences>({
|
const [preferences, setPreferences] = useState<UserPreferences>(defaultPreferences);
|
||||||
showThumbnails: true,
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,6 +33,8 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
|
|||||||
setPreferences(data);
|
setPreferences(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la récupération des préférences:", error);
|
console.error("Erreur lors de la récupération des préférences:", error);
|
||||||
|
// En cas d'erreur, on garde les préférences par défaut
|
||||||
|
setPreferences(defaultPreferences);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ const preferencesSchema = new mongoose.Schema(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
cacheMode: {
|
||||||
|
type: String,
|
||||||
|
enum: ["memory", "file"],
|
||||||
|
default: "memory",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ interface User {
|
|||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
showThumbnails: boolean;
|
showThumbnails: boolean;
|
||||||
|
cacheMode: "memory" | "file";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPreferences: UserPreferences = {
|
||||||
|
showThumbnails: true,
|
||||||
|
cacheMode: "memory",
|
||||||
|
};
|
||||||
|
|
||||||
export class PreferencesService {
|
export class PreferencesService {
|
||||||
private static async getCurrentUser(): Promise<User> {
|
private static async getCurrentUser(): Promise<User> {
|
||||||
const userCookie = cookies().get("stripUser");
|
const userCookie = cookies().get("stripUser");
|
||||||
@@ -27,39 +33,46 @@ export class PreferencesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getPreferences(): Promise<UserPreferences> {
|
static async getPreferences(userId: string): Promise<UserPreferences> {
|
||||||
await connectDB();
|
try {
|
||||||
const user = await this.getCurrentUser();
|
const preferences = await PreferencesModel.findOne({ userId });
|
||||||
|
if (!preferences) {
|
||||||
const preferences = await PreferencesModel.findOne({ userId: user.id });
|
return {
|
||||||
if (!preferences) {
|
showThumbnails: true,
|
||||||
// Créer les préférences par défaut si elles n'existent pas
|
cacheMode: "memory",
|
||||||
const defaultPreferences = await PreferencesModel.create({
|
};
|
||||||
userId: user.id,
|
}
|
||||||
showThumbnails: true,
|
|
||||||
});
|
|
||||||
return {
|
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> {
|
static async updatePreferences(
|
||||||
await connectDB();
|
userId: string,
|
||||||
const user = await this.getCurrentUser();
|
preferences: Partial<UserPreferences>
|
||||||
|
): Promise<UserPreferences> {
|
||||||
|
try {
|
||||||
|
const updatedPreferences = await PreferencesModel.findOneAndUpdate(
|
||||||
|
{ userId },
|
||||||
|
{ $set: preferences },
|
||||||
|
{ new: true, upsert: true }
|
||||||
|
);
|
||||||
|
|
||||||
const updatedPreferences = await PreferencesModel.findOneAndUpdate(
|
return {
|
||||||
{ userId: user.id },
|
showThumbnails: updatedPreferences.showThumbnails,
|
||||||
{ $set: preferences },
|
cacheMode: updatedPreferences.cacheMode || "memory",
|
||||||
{ new: true, upsert: true }
|
};
|
||||||
);
|
} catch (error) {
|
||||||
|
console.error("Error updating preferences:", error);
|
||||||
return {
|
throw error;
|
||||||
showThumbnails: updatedPreferences.showThumbnails,
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
type CacheMode = "file" | "memory";
|
||||||
|
|
||||||
|
interface CacheConfig {
|
||||||
|
mode: CacheMode;
|
||||||
|
}
|
||||||
|
|
||||||
class ServerCacheService {
|
class ServerCacheService {
|
||||||
private static instance: 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
|
// Configuration des temps de cache en millisecondes
|
||||||
private static readonly fiveMinutes = 5 * 60 * 1000;
|
private static readonly fiveMinutes = 5 * 60 * 1000;
|
||||||
@@ -21,7 +34,83 @@ class ServerCacheService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private constructor() {
|
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 {
|
public static getInstance(): ServerCacheService {
|
||||||
@@ -39,44 +128,203 @@ class ServerCacheService {
|
|||||||
return ServerCacheService.DEFAULT_TTL[type];
|
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
|
* 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 {
|
set(key: string, data: any, type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"): void {
|
||||||
this.cache.set(key, {
|
const cacheData = {
|
||||||
data,
|
data,
|
||||||
expiry: Date.now() + this.getTTL(type),
|
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
|
* Récupère des données du cache si elles sont valides
|
||||||
*/
|
*/
|
||||||
get(key: string): any | null {
|
get(key: string): any | null {
|
||||||
const cached = this.cache.get(key);
|
if (this.config.mode === "memory") {
|
||||||
if (!cached) return null;
|
const cached = this.memoryCache.get(key);
|
||||||
|
if (!cached) return null;
|
||||||
|
|
||||||
const now = Date.now();
|
if (cached.expiry > Date.now()) {
|
||||||
if (cached.expiry > now) {
|
return cached.data;
|
||||||
return cached.data;
|
}
|
||||||
|
|
||||||
|
this.memoryCache.delete(key);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cache.delete(key);
|
const filePath = this.getCacheFilePath(key);
|
||||||
return null;
|
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
|
* Supprime une entrée du cache
|
||||||
*/
|
*/
|
||||||
delete(key: string): void {
|
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
|
* Vide le cache
|
||||||
*/
|
*/
|
||||||
clear(): void {
|
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>,
|
fetcher: () => Promise<T>,
|
||||||
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
|
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const now = Date.now();
|
const cached = this.get(key);
|
||||||
const cached = this.cache.get(key);
|
if (cached !== null) {
|
||||||
|
return cached as T;
|
||||||
if (cached && cached.expiry > now) {
|
|
||||||
return cached.data as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetcher();
|
const data = await fetcher();
|
||||||
this.cache.set(key, {
|
this.set(key, data, type);
|
||||||
data,
|
|
||||||
expiry: now + this.getTTL(type),
|
|
||||||
});
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -107,7 +350,7 @@ class ServerCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invalidate(key: string): void {
|
invalidate(key: string): void {
|
||||||
this.cache.delete(key);
|
this.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user