From f636a7b11224e16b5d81f7329934426f31bbab3b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 17 Oct 2025 08:23:27 +0200 Subject: [PATCH] feat: add cache size retrieval functionality and display in CacheSettings component --- src/app/api/komga/cache/size/route.ts | 31 +++++++ src/components/settings/CacheSettings.tsx | 107 +++++++++++++++++++++- src/constants/errorCodes.ts | 1 + src/constants/errorMessages.ts | 1 + src/i18n/messages/en/common.json | 22 ++++- src/i18n/messages/fr/common.json | 8 ++ src/lib/services/server-cache.service.ts | 87 ++++++++++++++++++ 7 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 src/app/api/komga/cache/size/route.ts diff --git a/src/app/api/komga/cache/size/route.ts b/src/app/api/komga/cache/size/route.ts new file mode 100644 index 0000000..79580fe --- /dev/null +++ b/src/app/api/komga/cache/size/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import type { ServerCacheService } from "@/lib/services/server-cache.service"; +import { getServerCacheService } from "@/lib/services/server-cache.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { getErrorMessage } from "@/utils/errors"; + +export async function GET() { + try { + const cacheService: ServerCacheService = await getServerCacheService(); + const { sizeInBytes, itemCount } = await cacheService.getCacheSize(); + + return NextResponse.json({ + sizeInBytes, + itemCount, + mode: cacheService.getCacheMode() + }); + } catch (error) { + console.error("Erreur lors de la récupération de la taille du cache:", error); + return NextResponse.json( + { + error: { + code: ERROR_CODES.CACHE.SIZE_FETCH_ERROR, + name: "Cache size fetch error", + message: getErrorMessage(ERROR_CODES.CACHE.SIZE_FETCH_ERROR), + }, + }, + { status: 500 } + ); + } +} + diff --git a/src/components/settings/CacheSettings.tsx b/src/components/settings/CacheSettings.tsx index 41ebdb4..2b9617f 100644 --- a/src/components/settings/CacheSettings.tsx +++ b/src/components/settings/CacheSettings.tsx @@ -1,9 +1,9 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useTranslate } from "@/hooks/useTranslate"; import { useToast } from "@/components/ui/use-toast"; -import { Trash2, Loader2 } from "lucide-react"; +import { Trash2, Loader2, HardDrive } from "lucide-react"; import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch"; import { Label } from "@/components/ui/label"; import type { TTLConfigData } from "@/types/komga"; @@ -12,11 +12,19 @@ interface CacheSettingsProps { initialTTLConfig: TTLConfigData | null; } +interface CacheSizeInfo { + sizeInBytes: number; + itemCount: number; +} + export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { const { t } = useTranslate(); const { toast } = useToast(); const [isCacheClearing, setIsCacheClearing] = useState(false); const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false); + const [serverCacheSize, setServerCacheSize] = useState(null); + const [swCacheSize, setSwCacheSize] = useState(null); + const [isLoadingCacheSize, setIsLoadingCacheSize] = useState(true); const [ttlConfig, setTTLConfig] = useState( initialTTLConfig || { defaultTTL: 5, @@ -28,6 +36,58 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { } ); + const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; + }; + + const fetchCacheSize = async () => { + setIsLoadingCacheSize(true); + try { + // Récupérer la taille du cache serveur + const serverResponse = await fetch("/api/komga/cache/size"); + if (serverResponse.ok) { + const serverData = await serverResponse.json(); + setServerCacheSize({ + sizeInBytes: serverData.sizeInBytes, + itemCount: serverData.itemCount, + }); + } + + // Calculer la taille du cache Service Worker + if ("caches" in window) { + const cacheNames = await caches.keys(); + let totalSize = 0; + + for (const cacheName of cacheNames) { + const cache = await caches.open(cacheName); + const requests = await cache.keys(); + + for (const request of requests) { + const response = await cache.match(request); + if (response) { + const blob = await response.clone().blob(); + totalSize += blob.size; + } + } + } + + setSwCacheSize(totalSize); + } + } catch (error) { + console.error("Erreur lors de la récupération de la taille du cache:", error); + } finally { + setIsLoadingCacheSize(false); + } + }; + + useEffect(() => { + fetchCacheSize(); + }, []); + const handleClearCache = async () => { setIsCacheClearing(true); @@ -45,6 +105,9 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { title: t("settings.cache.title"), description: t("settings.cache.messages.cleared"), }); + + // Rafraîchir la taille du cache + await fetchCacheSize(); } catch (error) { console.error("Erreur:", error); toast({ @@ -67,6 +130,9 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { title: t("settings.cache.title"), description: t("settings.cache.messages.serviceWorkerCleared"), }); + + // Rafraîchir la taille du cache + await fetchCacheSize(); } } catch (error) { console.error("Erreur lors de la suppression des caches:", error); @@ -138,6 +204,43 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) { + {/* Informations sur la taille du cache */} +
+
+ + {t("settings.cache.size.title")} +
+ + {isLoadingCacheSize ? ( +
{t("settings.cache.size.loading")}
+ ) : ( +
+
+
{t("settings.cache.size.server")}
+ {serverCacheSize ? ( +
+
{formatBytes(serverCacheSize.sizeInBytes)}
+
+ {t("settings.cache.size.items", { count: serverCacheSize.itemCount })} +
+
+ ) : ( +
{t("settings.cache.size.error")}
+ )} +
+ +
+
{t("settings.cache.size.serviceWorker")}
+ {swCacheSize !== null ? ( +
{formatBytes(swCacheSize)}
+ ) : ( +
{t("settings.cache.size.error")}
+ )} +
+
+ )} +
+ {/* Formulaire TTL */}
diff --git a/src/constants/errorCodes.ts b/src/constants/errorCodes.ts index 4f574f6..f4d5d5d 100644 --- a/src/constants/errorCodes.ts +++ b/src/constants/errorCodes.ts @@ -70,6 +70,7 @@ export const ERROR_CODES = { MODE_FETCH_ERROR: "CACHE_MODE_FETCH_ERROR", MODE_UPDATE_ERROR: "CACHE_MODE_UPDATE_ERROR", INVALID_MODE: "CACHE_INVALID_MODE", + SIZE_FETCH_ERROR: "CACHE_SIZE_FETCH_ERROR", }, UI: { TABS_TRIGGER_ERROR: "UI_TABS_TRIGGER_ERROR", diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index e7a36d3..b87f1d6 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -67,6 +67,7 @@ export const ERROR_MESSAGES: Record = { [ERROR_CODES.CACHE.MODE_FETCH_ERROR]: "⚙️ Error fetching cache mode", [ERROR_CODES.CACHE.MODE_UPDATE_ERROR]: "⚙️ Error updating cache mode", [ERROR_CODES.CACHE.INVALID_MODE]: "⚠️ Invalid cache mode. Must be 'file' or 'memory'", + [ERROR_CODES.CACHE.SIZE_FETCH_ERROR]: "📊 Error fetching cache size", // UI [ERROR_CODES.UI.TABS_TRIGGER_ERROR]: "🔄 TabsTrigger must be used within a Tabs component", diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index efbb445..57ab1ef 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -117,6 +117,14 @@ "label": "Cache mode", "description": "Memory cache is faster but doesn't persist between restarts" }, + "size": { + "title": "Cache size", + "server": "Server cache", + "serviceWorker": "Service worker cache", + "items": "{count} item(s)", + "loading": "Loading...", + "error": "Error loading" + }, "ttl": { "default": "Default TTL (minutes)", "home": "Home page TTL", @@ -128,16 +136,20 @@ "buttons": { "saveTTL": "Save TTL", "clear": "Clear cache", - "clearing": "Clearing..." + "clearing": "Clearing...", + "clearServiceWorker": "Clear service worker cache", + "clearingServiceWorker": "Clearing service worker cache..." }, "messages": { "ttlSaved": "TTL configuration saved successfully", - "cleared": "Server cache cleared successfully" + "cleared": "Server cache cleared successfully", + "serviceWorkerCleared": "Service worker cache cleared successfully" }, "error": { - "title": "Error clearing server cache", - "message": "An error occurred while clearing the server cache", - "ttl": "Error saving TTL configuration" + "title": "Error clearing cache", + "message": "An error occurred while clearing the cache", + "ttl": "Error saving TTL configuration", + "serviceWorkerMessage": "An error occurred while clearing the service worker cache" } } }, diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index d9af89d..258cadc 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -117,6 +117,14 @@ "label": "Mode de cache", "description": "Le cache en mémoire est plus rapide mais ne persiste pas entre les redémarrages" }, + "size": { + "title": "Taille du cache", + "server": "Cache serveur", + "serviceWorker": "Cache service worker", + "items": "{count} élément(s)", + "loading": "Chargement...", + "error": "Erreur lors du chargement" + }, "ttl": { "default": "TTL par défaut (minutes)", "home": "TTL page d'accueil", diff --git a/src/lib/services/server-cache.service.ts b/src/lib/services/server-cache.service.ts index 2e73ace..2f2bb43 100644 --- a/src/lib/services/server-cache.service.ts +++ b/src/lib/services/server-cache.service.ts @@ -424,6 +424,93 @@ class ServerCacheService { invalidate(key: string): void { this.delete(key); } + + /** + * Calcule la taille approximative d'un objet en mémoire + */ + private calculateObjectSize(obj: unknown): number { + if (obj === null || obj === undefined) return 0; + + // Si c'est un Buffer, utiliser sa taille réelle + if (Buffer.isBuffer(obj)) { + return obj.length; + } + + // Si c'est un objet avec une propriété buffer (comme ImageResponse) + if (typeof obj === "object" && obj !== null) { + const objAny = obj as any; + if (objAny.buffer && Buffer.isBuffer(objAny.buffer)) { + // Taille du buffer + taille approximative des autres propriétés + let size = objAny.buffer.length; + // Ajouter la taille du contentType si présent + if (objAny.contentType && typeof objAny.contentType === "string") { + size += objAny.contentType.length * 2; // UTF-16 + } + return size; + } + } + + // Pour les autres types, utiliser JSON.stringify comme approximation + try { + return JSON.stringify(obj).length * 2; // x2 pour UTF-16 + } catch { + // Si l'objet n'est pas sérialisable, retourner une estimation + return 1000; // 1KB par défaut + } + } + + /** + * Calcule la taille du cache + */ + async getCacheSize(): Promise<{ sizeInBytes: number; itemCount: number }> { + if (this.config.mode === "memory") { + // Calculer la taille approximative en mémoire + let sizeInBytes = 0; + let itemCount = 0; + + this.memoryCache.forEach((value) => { + if (value.expiry > Date.now()) { + itemCount++; + // Calculer la taille du data + expiry (8 bytes pour le timestamp) + sizeInBytes += this.calculateObjectSize(value.data) + 8; + } + }); + + return { sizeInBytes, itemCount }; + } + + // Calculer la taille du cache sur disque + let sizeInBytes = 0; + let itemCount = 0; + + const calculateDirectorySize = (dirPath: string): void => { + 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()) { + calculateDirectorySize(itemPath); + } else if (stats.isFile() && item.endsWith(".json")) { + sizeInBytes += stats.size; + itemCount++; + } + } catch (error) { + console.error(`Could not access ${itemPath}:`, error); + } + } + }; + + if (fs.existsSync(this.cacheDir)) { + calculateDirectorySize(this.cacheDir); + } + + return { sizeInBytes, itemCount }; + } } // Créer une instance initialisée du service