feat: add cache entries API and enhance CacheSettings component with server and service worker cache previews
This commit is contained in:
27
src/app/api/komga/cache/entries/route.ts
vendored
Normal file
27
src/app/api/komga/cache/entries/route.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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 entries = await cacheService.getCacheEntries();
|
||||||
|
|
||||||
|
return NextResponse.json({ entries });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des entrées du cache:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: ERROR_CODES.CACHE.SIZE_FETCH_ERROR,
|
||||||
|
name: "Cache entries fetch error",
|
||||||
|
message: getErrorMessage(ERROR_CODES.CACHE.SIZE_FETCH_ERROR),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Trash2, Loader2, HardDrive } from "lucide-react";
|
import { Trash2, Loader2, HardDrive, List, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch";
|
import { CacheModeSwitch } from "@/components/settings/CacheModeSwitch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import type { TTLConfigData } from "@/types/komga";
|
import type { TTLConfigData } from "@/types/komga";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface CacheSettingsProps {
|
interface CacheSettingsProps {
|
||||||
initialTTLConfig: TTLConfigData | null;
|
initialTTLConfig: TTLConfigData | null;
|
||||||
@@ -18,6 +19,19 @@ interface CacheSizeInfo {
|
|||||||
itemCount: number;
|
itemCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
key: string;
|
||||||
|
size: number;
|
||||||
|
expiry: number;
|
||||||
|
isExpired: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceWorkerCacheEntry {
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
cacheName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -26,6 +40,13 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
|
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
|
||||||
const [swCacheSize, setSwCacheSize] = useState<number | null>(null);
|
const [swCacheSize, setSwCacheSize] = useState<number | null>(null);
|
||||||
const [isLoadingCacheSize, setIsLoadingCacheSize] = useState(true);
|
const [isLoadingCacheSize, setIsLoadingCacheSize] = useState(true);
|
||||||
|
const [cacheEntries, setCacheEntries] = useState<CacheEntry[]>([]);
|
||||||
|
const [isLoadingEntries, setIsLoadingEntries] = useState(false);
|
||||||
|
const [showEntries, setShowEntries] = useState(false);
|
||||||
|
const [swCacheEntries, setSwCacheEntries] = useState<ServiceWorkerCacheEntry[]>([]);
|
||||||
|
const [isLoadingSwEntries, setIsLoadingSwEntries] = useState(false);
|
||||||
|
const [showSwEntries, setShowSwEntries] = useState(false);
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
||||||
initialTTLConfig || {
|
initialTTLConfig || {
|
||||||
defaultTTL: 5,
|
defaultTTL: 5,
|
||||||
@@ -45,6 +66,35 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDate = (timestamp: number): string => {
|
||||||
|
return new Date(timestamp).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeRemaining = (expiry: number): string => {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = expiry - now;
|
||||||
|
|
||||||
|
if (diff < 0) return t("settings.cache.entries.expired");
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) return t("settings.cache.entries.daysRemaining", { count: days });
|
||||||
|
if (hours > 0) return t("settings.cache.entries.hoursRemaining", { count: hours });
|
||||||
|
if (minutes > 0) return t("settings.cache.entries.minutesRemaining", { count: minutes });
|
||||||
|
return t("settings.cache.entries.lessThanMinute");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCacheType = (key: string): string => {
|
||||||
|
if (key.includes("/home")) return "HOME";
|
||||||
|
if (key.includes("/libraries")) return "LIBRARIES";
|
||||||
|
if (key.includes("/series/")) return "SERIES";
|
||||||
|
if (key.includes("/books/")) return "BOOKS";
|
||||||
|
if (key.includes("/images/")) return "IMAGES";
|
||||||
|
return "DEFAULT";
|
||||||
|
};
|
||||||
|
|
||||||
const fetchCacheSize = async () => {
|
const fetchCacheSize = async () => {
|
||||||
setIsLoadingCacheSize(true);
|
setIsLoadingCacheSize(true);
|
||||||
try {
|
try {
|
||||||
@@ -85,6 +135,114 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchCacheEntries = async () => {
|
||||||
|
setIsLoadingEntries(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/cache/entries");
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setCacheEntries(data.entries);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des entrées du cache:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingEntries(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleShowEntries = () => {
|
||||||
|
if (!showEntries && cacheEntries.length === 0) {
|
||||||
|
fetchCacheEntries();
|
||||||
|
}
|
||||||
|
setShowEntries(!showEntries);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSwCacheEntries = async () => {
|
||||||
|
setIsLoadingSwEntries(true);
|
||||||
|
try {
|
||||||
|
if ("caches" in window) {
|
||||||
|
const entries: ServiceWorkerCacheEntry[] = [];
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
|
||||||
|
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();
|
||||||
|
entries.push({
|
||||||
|
url: request.url,
|
||||||
|
size: blob.size,
|
||||||
|
cacheName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSwCacheEntries(entries);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des entrées du cache SW:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSwEntries(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleShowSwEntries = () => {
|
||||||
|
if (!showSwEntries && swCacheEntries.length === 0) {
|
||||||
|
fetchSwCacheEntries();
|
||||||
|
}
|
||||||
|
setShowSwEntries(!showSwEntries);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstPathSegment = (url: string): string => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const path = urlObj.pathname;
|
||||||
|
const segments = path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
if (segments.length === 0) return '/';
|
||||||
|
|
||||||
|
return `/${segments[0]}`;
|
||||||
|
} catch {
|
||||||
|
return 'Autres';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupEntriesByPath = (entries: ServiceWorkerCacheEntry[]) => {
|
||||||
|
const grouped = entries.reduce(
|
||||||
|
(acc, entry) => {
|
||||||
|
const pathGroup = getFirstPathSegment(entry.url);
|
||||||
|
if (!acc[pathGroup]) {
|
||||||
|
acc[pathGroup] = [];
|
||||||
|
}
|
||||||
|
acc[pathGroup].push(entry);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, ServiceWorkerCacheEntry[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trier chaque groupe par taille décroissante
|
||||||
|
Object.keys(grouped).forEach((key) => {
|
||||||
|
grouped[key].sort((a, b) => b.size - a.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalSizeByType = (entries: ServiceWorkerCacheEntry[]): number => {
|
||||||
|
return entries.reduce((sum, entry) => sum + entry.size, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
setExpandedGroups((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[groupName]: !prev[groupName],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCacheSize();
|
fetchCacheSize();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -107,8 +265,14 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
description: t("settings.cache.messages.cleared"),
|
description: t("settings.cache.messages.cleared"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rafraîchir la taille du cache
|
// Rafraîchir la taille du cache et les entrées
|
||||||
await fetchCacheSize();
|
await fetchCacheSize();
|
||||||
|
if (showEntries) {
|
||||||
|
await fetchCacheEntries();
|
||||||
|
}
|
||||||
|
if (showSwEntries) {
|
||||||
|
await fetchSwCacheEntries();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur:", error);
|
console.error("Erreur:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -132,8 +296,14 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
description: t("settings.cache.messages.serviceWorkerCleared"),
|
description: t("settings.cache.messages.serviceWorkerCleared"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rafraîchir la taille du cache
|
// Rafraîchir la taille du cache et les entrées
|
||||||
await fetchCacheSize();
|
await fetchCacheSize();
|
||||||
|
if (showEntries) {
|
||||||
|
await fetchCacheEntries();
|
||||||
|
}
|
||||||
|
if (showSwEntries) {
|
||||||
|
await fetchSwCacheEntries();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression des caches:", error);
|
console.error("Erreur lors de la suppression des caches:", error);
|
||||||
@@ -242,6 +412,168 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Aperçu des entrées du cache serveur */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={toggleShowEntries}
|
||||||
|
className="w-full flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
{t("settings.cache.entries.serverTitle")}
|
||||||
|
</span>
|
||||||
|
{showEntries ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showEntries && (
|
||||||
|
<div className="rounded-md border bg-muted/30 backdrop-blur-md">
|
||||||
|
{isLoadingEntries ? (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
|
||||||
|
{t("settings.cache.entries.loading")}
|
||||||
|
</div>
|
||||||
|
) : cacheEntries.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
{t("settings.cache.entries.empty")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
<div className="divide-y">
|
||||||
|
{cacheEntries.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`p-3 space-y-1 ${entry.isExpired ? "opacity-50" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-mono text-xs truncate" title={entry.key}>
|
||||||
|
{entry.key}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium">
|
||||||
|
{getCacheType(entry.key)}
|
||||||
|
</span>
|
||||||
|
<span>{formatBytes(entry.size)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right text-xs">
|
||||||
|
<div
|
||||||
|
className={`font-medium ${entry.isExpired ? "text-destructive" : "text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
{getTimeRemaining(entry.expiry)}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground/70" title={formatDate(entry.expiry)}>
|
||||||
|
{new Date(entry.expiry).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aperçu des entrées du cache service worker */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={toggleShowSwEntries}
|
||||||
|
className="w-full flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
{t("settings.cache.entries.serviceWorkerTitle")}
|
||||||
|
</span>
|
||||||
|
{showSwEntries ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showSwEntries && (
|
||||||
|
<div className="rounded-md border bg-muted/30 backdrop-blur-md">
|
||||||
|
{isLoadingSwEntries ? (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
|
||||||
|
{t("settings.cache.entries.loading")}
|
||||||
|
</div>
|
||||||
|
) : swCacheEntries.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
{t("settings.cache.entries.empty")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
{(() => {
|
||||||
|
const grouped = groupEntriesByPath(swCacheEntries);
|
||||||
|
return (
|
||||||
|
<div className="divide-y">
|
||||||
|
{Object.entries(grouped).map(([pathGroup, entries]) => {
|
||||||
|
const isExpanded = expandedGroups[pathGroup];
|
||||||
|
return (
|
||||||
|
<div key={pathGroup} className="p-3 space-y-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleGroup(pathGroup)}
|
||||||
|
className="w-full flex items-center justify-between hover:bg-muted/50 rounded p-1 -m-1 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm flex items-center gap-2">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center rounded-full bg-blue-500/10 px-2 py-0.5 text-xs font-medium font-mono">
|
||||||
|
{pathGroup}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
({entries.length} {entries.length > 1 ? "éléments" : "élément"})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground font-medium">
|
||||||
|
{formatBytes(getTotalSizeByType(entries))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="space-y-1 pl-2">
|
||||||
|
{entries.map((entry, index) => (
|
||||||
|
<div key={index} className="py-1">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-mono text-xs truncate text-muted-foreground" title={entry.url}>
|
||||||
|
{entry.url.replace(/^https?:\/\/[^/]+/, "")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatBytes(entry.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -164,6 +164,18 @@
|
|||||||
"message": "An error occurred while clearing the cache",
|
"message": "An error occurred while clearing the cache",
|
||||||
"ttl": "Error saving TTL configuration",
|
"ttl": "Error saving TTL configuration",
|
||||||
"serviceWorkerMessage": "An error occurred while clearing the service worker cache"
|
"serviceWorkerMessage": "An error occurred while clearing the service worker cache"
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"title": "Cache content preview",
|
||||||
|
"serverTitle": "Server cache preview",
|
||||||
|
"serviceWorkerTitle": "Service worker cache preview",
|
||||||
|
"loading": "Loading entries...",
|
||||||
|
"empty": "No entries in cache",
|
||||||
|
"expired": "Expired",
|
||||||
|
"daysRemaining": "{count} day(s) remaining",
|
||||||
|
"hoursRemaining": "{count} hour(s) remaining",
|
||||||
|
"minutesRemaining": "{count} minute(s) remaining",
|
||||||
|
"lessThanMinute": "Less than a minute"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -164,6 +164,18 @@
|
|||||||
"message": "Une erreur est survenue lors de la suppression du cache",
|
"message": "Une erreur est survenue lors de la suppression du cache",
|
||||||
"ttl": "Erreur lors de la sauvegarde de la configuration TTL",
|
"ttl": "Erreur lors de la sauvegarde de la configuration TTL",
|
||||||
"serviceWorkerMessage": "Une erreur est survenue lors de la suppression du cache du service worker"
|
"serviceWorkerMessage": "Une erreur est survenue lors de la suppression du cache du service worker"
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"title": "Aperçu du contenu du cache",
|
||||||
|
"serverTitle": "Aperçu du cache serveur",
|
||||||
|
"serviceWorkerTitle": "Aperçu du cache service worker",
|
||||||
|
"loading": "Chargement des entrées...",
|
||||||
|
"empty": "Aucune entrée dans le cache",
|
||||||
|
"expired": "Expiré",
|
||||||
|
"daysRemaining": "{count} jour(s) restant(s)",
|
||||||
|
"hoursRemaining": "{count} heure(s) restante(s)",
|
||||||
|
"minutesRemaining": "{count} minute(s) restante(s)",
|
||||||
|
"lessThanMinute": "Moins d'une minute"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -593,6 +593,77 @@ class ServerCacheService {
|
|||||||
|
|
||||||
return { sizeInBytes, itemCount };
|
return { sizeInBytes, itemCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les entrées du cache avec leurs détails
|
||||||
|
*/
|
||||||
|
async getCacheEntries(): Promise<
|
||||||
|
Array<{
|
||||||
|
key: string;
|
||||||
|
size: number;
|
||||||
|
expiry: number;
|
||||||
|
isExpired: boolean;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const entries: Array<{
|
||||||
|
key: string;
|
||||||
|
size: number;
|
||||||
|
expiry: number;
|
||||||
|
isExpired: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (this.config.mode === "memory") {
|
||||||
|
this.memoryCache.forEach((value, key) => {
|
||||||
|
const size = this.calculateObjectSize(value.data) + 8;
|
||||||
|
entries.push({
|
||||||
|
key,
|
||||||
|
size,
|
||||||
|
expiry: value.expiry,
|
||||||
|
isExpired: value.expiry <= Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const collectEntries = (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()) {
|
||||||
|
collectEntries(itemPath);
|
||||||
|
} else if (stats.isFile() && item.endsWith(".json")) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(itemPath, "utf-8");
|
||||||
|
const cached = JSON.parse(content);
|
||||||
|
const key = path.relative(this.cacheDir, itemPath).slice(0, -5);
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
key,
|
||||||
|
size: stats.size,
|
||||||
|
expiry: cached.expiry,
|
||||||
|
isExpired: cached.expiry <= Date.now(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not parse file ${itemPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not access ${itemPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fs.existsSync(this.cacheDir)) {
|
||||||
|
collectEntries(this.cacheDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.sort((a, b) => b.expiry - a.expiry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer une instance initialisée du service
|
// Créer une instance initialisée du service
|
||||||
|
|||||||
Reference in New Issue
Block a user