feat: implement image caching mechanism with configurable cache duration and flush functionality
This commit is contained in:
11
Dockerfile
11
Dockerfile
@@ -67,11 +67,16 @@ COPY --from=builder /app/next-env.d.ts ./
|
|||||||
COPY --from=builder /app/tailwind.config.ts ./
|
COPY --from=builder /app/tailwind.config.ts ./
|
||||||
COPY --from=builder /app/scripts ./scripts
|
COPY --from=builder /app/scripts ./scripts
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY docker-entrypoint.sh ./
|
||||||
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
|
||||||
# Add non-root user for security
|
# Add non-root user for security
|
||||||
RUN addgroup --system --gid 1001 nodejs && \
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
adduser --system --uid 1001 nextjs && \
|
adduser --system --uid 1001 nextjs && \
|
||||||
mkdir -p /app/.cache && \
|
mkdir -p /app/.cache && \
|
||||||
chown -R nextjs:nodejs /app /app/.cache
|
chown -R nextjs:nodejs /app /app/.cache && \
|
||||||
|
chown nextjs:nodejs docker-entrypoint.sh
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
@@ -86,5 +91,5 @@ EXPOSE 3000
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s \
|
HEALTHCHECK --interval=30s --timeout=3s \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
# Start the application (init DB then start)
|
# Start the application (push schema, init DB, then start)
|
||||||
CMD ["pnpm", "start:prod"]
|
CMD ["./docker-entrypoint.sh"]
|
||||||
12
docker-entrypoint.sh
Normal file
12
docker-entrypoint.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔄 Pushing Prisma schema to database..."
|
||||||
|
npx prisma db push --skip-generate --accept-data-loss
|
||||||
|
|
||||||
|
echo "🔧 Initializing database..."
|
||||||
|
node scripts/init-db.mjs
|
||||||
|
|
||||||
|
echo "🚀 Starting application..."
|
||||||
|
exec pnpm start
|
||||||
|
|
||||||
@@ -43,16 +43,17 @@ model KomgaConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model TTLConfig {
|
model TTLConfig {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
userId String @unique
|
userId String @unique
|
||||||
defaultTTL Int @default(5)
|
defaultTTL Int @default(5)
|
||||||
homeTTL Int @default(5)
|
homeTTL Int @default(5)
|
||||||
librariesTTL Int @default(1440)
|
librariesTTL Int @default(1440)
|
||||||
seriesTTL Int @default(5)
|
seriesTTL Int @default(5)
|
||||||
booksTTL Int @default(5)
|
booksTTL Int @default(5)
|
||||||
imagesTTL Int @default(1440)
|
imagesTTL Int @default(1440)
|
||||||
createdAt DateTime @default(now())
|
imageCacheMaxAge Int @default(2592000) // 30 jours en secondes
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export async function POST(request: NextRequest) {
|
|||||||
seriesTTL: config.seriesTTL,
|
seriesTTL: config.seriesTTL,
|
||||||
booksTTL: config.booksTTL,
|
booksTTL: config.booksTTL,
|
||||||
imagesTTL: config.imagesTTL,
|
imagesTTL: config.imagesTTL,
|
||||||
|
imageCacheMaxAge: config.imageCacheMaxAge,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { usePathname } from "next/navigation";
|
|||||||
import { registerServiceWorker } from "@/lib/registerSW";
|
import { registerServiceWorker } from "@/lib/registerSW";
|
||||||
import { NetworkStatus } from "../ui/NetworkStatus";
|
import { NetworkStatus } from "../ui/NetworkStatus";
|
||||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
|
import { ImageCacheProvider } from "@/contexts/ImageCacheContext";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
|
|
||||||
// Routes qui ne nécessitent pas d'authentification
|
// Routes qui ne nécessitent pas d'authentification
|
||||||
@@ -135,38 +136,40 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
{/* Background fixe pour les images et gradients */}
|
<ImageCacheProvider>
|
||||||
{hasCustomBackground && (
|
{/* Background fixe pour les images et gradients */}
|
||||||
|
{hasCustomBackground && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 -z-10"
|
||||||
|
style={backgroundStyle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 -z-10"
|
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
|
||||||
style={backgroundStyle}
|
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
|
||||||
/>
|
>
|
||||||
)}
|
{!isPublicRoute && (
|
||||||
<div
|
<Header
|
||||||
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
|
onToggleSidebar={handleToggleSidebar}
|
||||||
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
|
onRefreshBackground={fetchRandomBook}
|
||||||
>
|
showRefreshBackground={preferences.background.type === "komga-random"}
|
||||||
{!isPublicRoute && (
|
/>
|
||||||
<Header
|
)}
|
||||||
onToggleSidebar={handleToggleSidebar}
|
{!isPublicRoute && (
|
||||||
onRefreshBackground={fetchRandomBook}
|
<Sidebar
|
||||||
showRefreshBackground={preferences.background.type === "komga-random"}
|
isOpen={isSidebarOpen}
|
||||||
/>
|
onClose={handleCloseSidebar}
|
||||||
)}
|
initialLibraries={initialLibraries}
|
||||||
{!isPublicRoute && (
|
initialFavorites={initialFavorites}
|
||||||
<Sidebar
|
userIsAdmin={userIsAdmin}
|
||||||
isOpen={isSidebarOpen}
|
/>
|
||||||
onClose={handleCloseSidebar}
|
)}
|
||||||
initialLibraries={initialLibraries}
|
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
|
||||||
initialFavorites={initialFavorites}
|
<InstallPWA />
|
||||||
userIsAdmin={userIsAdmin}
|
<Toaster />
|
||||||
/>
|
<NetworkStatus />
|
||||||
)}
|
</div>
|
||||||
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
|
</ImageCacheProvider>
|
||||||
<InstallPWA />
|
|
||||||
<Toaster />
|
|
||||||
<NetworkStatus />
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
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, List, ChevronDown, ChevronUp } from "lucide-react";
|
import { Trash2, Loader2, HardDrive, List, ChevronDown, ChevronUp, ImageOff } 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";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useImageCache } from "@/contexts/ImageCacheContext";
|
||||||
|
|
||||||
interface CacheSettingsProps {
|
interface CacheSettingsProps {
|
||||||
initialTTLConfig: TTLConfigData | null;
|
initialTTLConfig: TTLConfigData | null;
|
||||||
@@ -35,6 +36,7 @@ interface ServiceWorkerCacheEntry {
|
|||||||
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { flushImageCache } = useImageCache();
|
||||||
const [isCacheClearing, setIsCacheClearing] = useState(false);
|
const [isCacheClearing, setIsCacheClearing] = useState(false);
|
||||||
const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false);
|
const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false);
|
||||||
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
|
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
|
||||||
@@ -56,6 +58,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
seriesTTL: 5,
|
seriesTTL: 5,
|
||||||
booksTTL: 5,
|
booksTTL: 5,
|
||||||
imagesTTL: 1440,
|
imagesTTL: 1440,
|
||||||
|
imageCacheMaxAge: 2592000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -389,7 +392,15 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFlushImageCache = () => {
|
||||||
|
flushImageCache();
|
||||||
|
toast({
|
||||||
|
title: t("settings.cache.title"),
|
||||||
|
description: t("settings.cache.messages.imageCacheFlushed"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = event.target;
|
const { name, value } = event.target;
|
||||||
setTTLConfig((prev) => ({
|
setTTLConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -788,6 +799,30 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
className="flex h-9 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label htmlFor="imageCacheMaxAge" className="text-sm font-medium">
|
||||||
|
{t("settings.cache.ttl.imageCacheMaxAge.label")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("settings.cache.ttl.imageCacheMaxAge.description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
id="imageCacheMaxAge"
|
||||||
|
name="imageCacheMaxAge"
|
||||||
|
value={ttlConfig.imageCacheMaxAge}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background/70 backdrop-blur-md px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="0">{t("settings.cache.ttl.imageCacheMaxAge.options.noCache")}</option>
|
||||||
|
<option value="3600">{t("settings.cache.ttl.imageCacheMaxAge.options.oneHour")}</option>
|
||||||
|
<option value="86400">{t("settings.cache.ttl.imageCacheMaxAge.options.oneDay")}</option>
|
||||||
|
<option value="604800">{t("settings.cache.ttl.imageCacheMaxAge.options.oneWeek")}</option>
|
||||||
|
<option value="2592000">{t("settings.cache.ttl.imageCacheMaxAge.options.oneMonth")}</option>
|
||||||
|
<option value="31536000">{t("settings.cache.ttl.imageCacheMaxAge.options.oneYear")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -829,6 +864,16 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFlushImageCache}
|
||||||
|
className="flex-1 inline-flex items-center justify-center rounded-md bg-orange-500/90 backdrop-blur-md px-3 py-2 text-sm font-medium text-white ring-offset-background transition-colors hover:bg-orange-500/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ImageOff className="mr-2 h-4 w-4" />
|
||||||
|
{t("settings.cache.buttons.flushImageCache")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
|
|||||||
import { ProgressBar } from "./progress-bar";
|
import { ProgressBar } from "./progress-bar";
|
||||||
import type { BookCoverProps } from "./cover-utils";
|
import type { BookCoverProps } from "./cover-utils";
|
||||||
import { getImageUrl } from "./cover-utils";
|
import { getImageUrl } from "./cover-utils";
|
||||||
|
import { useImageUrl } from "@/hooks/useImageUrl";
|
||||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||||
import { MarkAsReadButton } from "./mark-as-read-button";
|
import { MarkAsReadButton } from "./mark-as-read-button";
|
||||||
import { MarkAsUnreadButton } from "./mark-as-unread-button";
|
import { MarkAsUnreadButton } from "./mark-as-unread-button";
|
||||||
@@ -59,7 +60,8 @@ export function BookCover({
|
|||||||
}: BookCoverProps) {
|
}: BookCoverProps) {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const imageUrl = getImageUrl("book", book.id);
|
const baseUrl = getImageUrl("book", book.id);
|
||||||
|
const imageUrl = useImageUrl(baseUrl);
|
||||||
const isCompleted = book.readProgress?.completed || false;
|
const isCompleted = book.readProgress?.completed || false;
|
||||||
|
|
||||||
const currentPage = ClientOfflineBookService.getCurrentPage(book);
|
const currentPage = ClientOfflineBookService.getCurrentPage(book);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export interface SeriesCoverProps extends BaseCoverProps {
|
|||||||
series: KomgaSeries;
|
series: KomgaSeries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère l'URL de base pour une image (sans cache version)
|
||||||
|
* Utilisez useImageUrl() dans les composants pour obtenir l'URL avec cache busting
|
||||||
|
*/
|
||||||
export function getImageUrl(type: "series" | "book", id: string) {
|
export function getImageUrl(type: "series" | "book", id: string) {
|
||||||
if (type === "series") {
|
if (type === "series") {
|
||||||
return `/api/komga/images/series/${id}/thumbnail`;
|
return `/api/komga/images/series/${id}/thumbnail`;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
|
|||||||
import { ProgressBar } from "./progress-bar";
|
import { ProgressBar } from "./progress-bar";
|
||||||
import type { SeriesCoverProps } from "./cover-utils";
|
import type { SeriesCoverProps } from "./cover-utils";
|
||||||
import { getImageUrl } from "./cover-utils";
|
import { getImageUrl } from "./cover-utils";
|
||||||
|
import { useImageUrl } from "@/hooks/useImageUrl";
|
||||||
|
|
||||||
export function SeriesCover({
|
export function SeriesCover({
|
||||||
series,
|
series,
|
||||||
@@ -11,7 +12,8 @@ export function SeriesCover({
|
|||||||
className,
|
className,
|
||||||
showProgressUi = true,
|
showProgressUi = true,
|
||||||
}: SeriesCoverProps) {
|
}: SeriesCoverProps) {
|
||||||
const imageUrl = getImageUrl("series", series.id);
|
const baseUrl = getImageUrl("series", series.id);
|
||||||
|
const imageUrl = useImageUrl(baseUrl);
|
||||||
const isCompleted = series.booksCount === series.booksReadCount;
|
const isCompleted = series.booksCount === series.booksReadCount;
|
||||||
|
|
||||||
const readBooks = series.booksReadCount;
|
const readBooks = series.booksReadCount;
|
||||||
|
|||||||
59
src/contexts/ImageCacheContext.tsx
Normal file
59
src/contexts/ImageCacheContext.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
interface ImageCacheContextType {
|
||||||
|
cacheVersion: string;
|
||||||
|
flushImageCache: () => void;
|
||||||
|
getImageUrl: (baseUrl: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageCacheContext = createContext<ImageCacheContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ImageCacheProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [cacheVersion, setCacheVersion] = useState<string>("");
|
||||||
|
|
||||||
|
// Initialiser la version depuis localStorage au montage
|
||||||
|
useEffect(() => {
|
||||||
|
const storedVersion = localStorage.getItem("imageCacheVersion");
|
||||||
|
if (storedVersion) {
|
||||||
|
setCacheVersion(storedVersion);
|
||||||
|
} else {
|
||||||
|
const newVersion = Date.now().toString();
|
||||||
|
setCacheVersion(newVersion);
|
||||||
|
localStorage.setItem("imageCacheVersion", newVersion);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const flushImageCache = useCallback(() => {
|
||||||
|
const newVersion = Date.now().toString();
|
||||||
|
setCacheVersion(newVersion);
|
||||||
|
localStorage.setItem("imageCacheVersion", newVersion);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("🗑️ Image cache flushed - new version:", newVersion);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getImageUrl = useCallback(
|
||||||
|
(baseUrl: string) => {
|
||||||
|
if (!cacheVersion) return baseUrl;
|
||||||
|
const separator = baseUrl.includes("?") ? "&" : "?";
|
||||||
|
return `${baseUrl}${separator}v=${cacheVersion}`;
|
||||||
|
},
|
||||||
|
[cacheVersion]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageCacheContext.Provider value={{ cacheVersion, flushImageCache, getImageUrl }}>
|
||||||
|
{children}
|
||||||
|
</ImageCacheContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImageCache() {
|
||||||
|
const context = useContext(ImageCacheContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useImageCache must be used within an ImageCacheProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
15
src/hooks/useImageUrl.ts
Normal file
15
src/hooks/useImageUrl.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useImageCache } from "@/contexts/ImageCacheContext";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook pour obtenir une URL d'image avec cache busting
|
||||||
|
* Ajoute automatiquement ?v={cacheVersion} à l'URL
|
||||||
|
*/
|
||||||
|
export function useImageUrl(baseUrl: string): string {
|
||||||
|
const { getImageUrl } = useImageCache();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return getImageUrl(baseUrl);
|
||||||
|
}, [baseUrl, getImageUrl]);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -145,19 +145,33 @@
|
|||||||
"libraries": "Libraries TTL",
|
"libraries": "Libraries TTL",
|
||||||
"series": "Series TTL",
|
"series": "Series TTL",
|
||||||
"books": "Books TTL",
|
"books": "Books TTL",
|
||||||
"images": "Images TTL"
|
"images": "Images TTL",
|
||||||
|
"imageCacheMaxAge": {
|
||||||
|
"label": "HTTP Image Cache Duration",
|
||||||
|
"description": "Duration for images to be cached in the browser",
|
||||||
|
"options": {
|
||||||
|
"noCache": "No cache (0s)",
|
||||||
|
"oneHour": "1 hour (3600s)",
|
||||||
|
"oneDay": "1 day (86400s)",
|
||||||
|
"oneWeek": "1 week (604800s)",
|
||||||
|
"oneMonth": "1 month (2592000s) - Recommended",
|
||||||
|
"oneYear": "1 year (31536000s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"saveTTL": "Save TTL",
|
"saveTTL": "Save TTL",
|
||||||
"clear": "Clear cache",
|
"clear": "Clear cache",
|
||||||
"clearing": "Clearing...",
|
"clearing": "Clearing...",
|
||||||
"clearServiceWorker": "Clear service worker cache",
|
"clearServiceWorker": "Clear service worker cache",
|
||||||
"clearingServiceWorker": "Clearing service worker cache..."
|
"clearingServiceWorker": "Clearing service worker cache...",
|
||||||
|
"flushImageCache": "Force reload images"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"ttlSaved": "TTL configuration saved successfully",
|
"ttlSaved": "TTL configuration saved successfully",
|
||||||
"cleared": "Server cache cleared successfully",
|
"cleared": "Server cache cleared successfully",
|
||||||
"serviceWorkerCleared": "Service worker cache cleared successfully"
|
"serviceWorkerCleared": "Service worker cache cleared successfully",
|
||||||
|
"imageCacheFlushed": "Images will be reloaded - refresh the page"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"title": "Error clearing cache",
|
"title": "Error clearing cache",
|
||||||
|
|||||||
@@ -145,19 +145,33 @@
|
|||||||
"libraries": "TTL bibliothèques",
|
"libraries": "TTL bibliothèques",
|
||||||
"series": "TTL séries",
|
"series": "TTL séries",
|
||||||
"books": "TTL tomes",
|
"books": "TTL tomes",
|
||||||
"images": "TTL images"
|
"images": "TTL images",
|
||||||
|
"imageCacheMaxAge": {
|
||||||
|
"label": "Durée du cache HTTP des images",
|
||||||
|
"description": "Durée de conservation des images dans le cache du navigateur",
|
||||||
|
"options": {
|
||||||
|
"noCache": "Aucun cache (0s)",
|
||||||
|
"oneHour": "1 heure (3600s)",
|
||||||
|
"oneDay": "1 jour (86400s)",
|
||||||
|
"oneWeek": "1 semaine (604800s)",
|
||||||
|
"oneMonth": "1 mois (2592000s) - Recommandé",
|
||||||
|
"oneYear": "1 an (31536000s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"saveTTL": "Sauvegarder les TTL",
|
"saveTTL": "Sauvegarder les TTL",
|
||||||
"clear": "Vider le cache",
|
"clear": "Vider le cache",
|
||||||
"clearing": "Suppression...",
|
"clearing": "Suppression...",
|
||||||
"clearServiceWorker": "Vider le cache du service worker",
|
"clearServiceWorker": "Vider le cache du service worker",
|
||||||
"clearingServiceWorker": "Suppression du cache service worker..."
|
"clearingServiceWorker": "Suppression du cache service worker...",
|
||||||
|
"flushImageCache": "Forcer le rechargement des images"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"ttlSaved": "La configuration des TTL a été sauvegardée avec succès",
|
"ttlSaved": "La configuration des TTL a été sauvegardée avec succès",
|
||||||
"cleared": "Cache serveur supprimé avec succès",
|
"cleared": "Cache serveur supprimé avec succès",
|
||||||
"serviceWorkerCleared": "Cache du service worker supprimé avec succès"
|
"serviceWorkerCleared": "Cache du service worker supprimé avec succès",
|
||||||
|
"imageCacheFlushed": "Les images seront rechargées - rafraîchissez la page"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"title": "Erreur lors de la suppression du cache",
|
"title": "Erreur lors de la suppression du cache",
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { BaseApiService } from "./base-api.service";
|
import { BaseApiService } from "./base-api.service";
|
||||||
import type { KomgaBook, KomgaBookWithPages } from "@/types/komga";
|
import type { KomgaBook, KomgaBookWithPages, TTLConfig } from "@/types/komga";
|
||||||
import type { ImageResponse } from "./image.service";
|
import type { ImageResponse } from "./image.service";
|
||||||
import { ImageService } from "./image.service";
|
import { ImageService } from "./image.service";
|
||||||
import { PreferencesService } from "./preferences.service";
|
import { PreferencesService } from "./preferences.service";
|
||||||
|
import { ConfigDBService } from "./config-db.service";
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
import { SeriesService } from "./series.service";
|
import { SeriesService } from "./series.service";
|
||||||
import type { Series } from "@/types/series";
|
import type { Series } from "@/types/series";
|
||||||
|
|
||||||
export class BookService extends BaseApiService {
|
export class BookService extends BaseApiService {
|
||||||
|
private static async getImageCacheMaxAge(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const ttlConfig: TTLConfig | null = await ConfigDBService.getTTLConfig();
|
||||||
|
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
|
||||||
|
return maxAge;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ImageCache] Error fetching TTL config:', error);
|
||||||
|
return 2592000; // 30 jours par défaut en cas d'erreur
|
||||||
|
}
|
||||||
|
}
|
||||||
static async getBook(bookId: string): Promise<KomgaBookWithPages> {
|
static async getBook(bookId: string): Promise<KomgaBookWithPages> {
|
||||||
try {
|
try {
|
||||||
return this.fetchWithCache<KomgaBookWithPages>(
|
return this.fetchWithCache<KomgaBookWithPages>(
|
||||||
@@ -102,10 +113,12 @@ export class BookService extends BaseApiService {
|
|||||||
response.buffer.byteOffset + response.buffer.byteLength
|
response.buffer.byteOffset + response.buffer.byteLength
|
||||||
) as ArrayBuffer;
|
) as ArrayBuffer;
|
||||||
|
|
||||||
|
const maxAge = await this.getImageCacheMaxAge();
|
||||||
|
|
||||||
return new Response(arrayBuffer, {
|
return new Response(arrayBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": response.contentType || "image/jpeg",
|
"Content-Type": response.contentType || "image/jpeg",
|
||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -117,6 +130,7 @@ export class BookService extends BaseApiService {
|
|||||||
try {
|
try {
|
||||||
// Récupérer les préférences de l'utilisateur
|
// Récupérer les préférences de l'utilisateur
|
||||||
const preferences = await PreferencesService.getPreferences();
|
const preferences = await PreferencesService.getPreferences();
|
||||||
|
const maxAge = await this.getImageCacheMaxAge();
|
||||||
|
|
||||||
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
||||||
if (preferences.showThumbnails) {
|
if (preferences.showThumbnails) {
|
||||||
@@ -124,7 +138,7 @@ export class BookService extends BaseApiService {
|
|||||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": response.contentType || "image/jpeg",
|
"Content-Type": response.contentType || "image/jpeg",
|
||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,10 +163,12 @@ export class BookService extends BaseApiService {
|
|||||||
const response: ImageResponse = await ImageService.getImage(
|
const response: ImageResponse = await ImageService.getImage(
|
||||||
`books/${bookId}/pages/${pageNumber}/thumbnail?zero_based=true`
|
`books/${bookId}/pages/${pageNumber}/thumbnail?zero_based=true`
|
||||||
);
|
);
|
||||||
|
const maxAge = await this.getImageCacheMaxAge();
|
||||||
|
|
||||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": response.contentType || "image/jpeg",
|
"Content-Type": response.contentType || "image/jpeg",
|
||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class ConfigDBService {
|
|||||||
seriesTTL: data.seriesTTL,
|
seriesTTL: data.seriesTTL,
|
||||||
booksTTL: data.booksTTL,
|
booksTTL: data.booksTTL,
|
||||||
imagesTTL: data.imagesTTL,
|
imagesTTL: data.imagesTTL,
|
||||||
|
imageCacheMaxAge: data.imageCacheMaxAge,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -99,6 +100,7 @@ export class ConfigDBService {
|
|||||||
seriesTTL: data.seriesTTL,
|
seriesTTL: data.seriesTTL,
|
||||||
booksTTL: data.booksTTL,
|
booksTTL: data.booksTTL,
|
||||||
imagesTTL: data.imagesTTL,
|
imagesTTL: data.imagesTTL,
|
||||||
|
imageCacheMaxAge: data.imageCacheMaxAge,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { BaseApiService } from "./base-api.service";
|
import { BaseApiService } from "./base-api.service";
|
||||||
import type { LibraryResponse } from "@/types/library";
|
import type { LibraryResponse } from "@/types/library";
|
||||||
import type { KomgaBook, KomgaSeries } from "@/types/komga";
|
import type { KomgaBook, KomgaSeries, TTLConfig } from "@/types/komga";
|
||||||
import { BookService } from "./book.service";
|
import { BookService } from "./book.service";
|
||||||
import type { ImageResponse } from "./image.service";
|
import type { ImageResponse } from "./image.service";
|
||||||
import { ImageService } from "./image.service";
|
import { ImageService } from "./image.service";
|
||||||
import { PreferencesService } from "./preferences.service";
|
import { PreferencesService } from "./preferences.service";
|
||||||
|
import { ConfigDBService } from "./config-db.service";
|
||||||
import { getServerCacheService } from "./server-cache.service";
|
import { getServerCacheService } from "./server-cache.service";
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
@@ -12,6 +13,16 @@ import type { UserPreferences } from "@/types/preferences";
|
|||||||
import type { ServerCacheService } from "./server-cache.service";
|
import type { ServerCacheService } from "./server-cache.service";
|
||||||
|
|
||||||
export class SeriesService extends BaseApiService {
|
export class SeriesService extends BaseApiService {
|
||||||
|
private static async getImageCacheMaxAge(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const ttlConfig: TTLConfig | null = await ConfigDBService.getTTLConfig();
|
||||||
|
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
|
||||||
|
return maxAge;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ImageCache] Error fetching TTL config:', error);
|
||||||
|
return 2592000; // 30 jours par défaut en cas d'erreur
|
||||||
|
}
|
||||||
|
}
|
||||||
static async getSeries(seriesId: string): Promise<KomgaSeries> {
|
static async getSeries(seriesId: string): Promise<KomgaSeries> {
|
||||||
try {
|
try {
|
||||||
return this.fetchWithCache<KomgaSeries>(
|
return this.fetchWithCache<KomgaSeries>(
|
||||||
@@ -179,6 +190,7 @@ export class SeriesService extends BaseApiService {
|
|||||||
try {
|
try {
|
||||||
// Récupérer les préférences de l'utilisateur
|
// Récupérer les préférences de l'utilisateur
|
||||||
const preferences: UserPreferences = await PreferencesService.getPreferences();
|
const preferences: UserPreferences = await PreferencesService.getPreferences();
|
||||||
|
const maxAge = await this.getImageCacheMaxAge();
|
||||||
|
|
||||||
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
||||||
if (preferences.showThumbnails) {
|
if (preferences.showThumbnails) {
|
||||||
@@ -186,7 +198,7 @@ export class SeriesService extends BaseApiService {
|
|||||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": response.contentType || "image/jpeg",
|
"Content-Type": response.contentType || "image/jpeg",
|
||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface TTLConfigData {
|
|||||||
seriesTTL: number;
|
seriesTTL: number;
|
||||||
booksTTL: number;
|
booksTTL: number;
|
||||||
imagesTTL: number;
|
imagesTTL: number;
|
||||||
|
imageCacheMaxAge: number; // en secondes
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TTLConfig extends TTLConfigData {
|
export interface TTLConfig extends TTLConfigData {
|
||||||
|
|||||||
Reference in New Issue
Block a user