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/scripts ./scripts
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh ./
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
|
||||
# Add non-root user for security
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs && \
|
||||
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
|
||||
|
||||
@@ -86,5 +91,5 @@ EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=3s \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
||||
|
||||
# Start the application (init DB then start)
|
||||
CMD ["pnpm", "start:prod"]
|
||||
# Start the application (push schema, init DB, then start)
|
||||
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 {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
userId String @unique
|
||||
defaultTTL Int @default(5)
|
||||
homeTTL Int @default(5)
|
||||
librariesTTL Int @default(1440)
|
||||
seriesTTL Int @default(5)
|
||||
booksTTL Int @default(5)
|
||||
imagesTTL Int @default(1440)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
userId String @unique
|
||||
defaultTTL Int @default(5)
|
||||
homeTTL Int @default(5)
|
||||
librariesTTL Int @default(1440)
|
||||
seriesTTL Int @default(5)
|
||||
booksTTL Int @default(5)
|
||||
imagesTTL Int @default(1440)
|
||||
imageCacheMaxAge Int @default(2592000) // 30 jours en secondes
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function POST(request: NextRequest) {
|
||||
seriesTTL: config.seriesTTL,
|
||||
booksTTL: config.booksTTL,
|
||||
imagesTTL: config.imagesTTL,
|
||||
imageCacheMaxAge: config.imageCacheMaxAge,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { usePathname } from "next/navigation";
|
||||
import { registerServiceWorker } from "@/lib/registerSW";
|
||||
import { NetworkStatus } from "../ui/NetworkStatus";
|
||||
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||
import { ImageCacheProvider } from "@/contexts/ImageCacheContext";
|
||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||
|
||||
// Routes qui ne nécessitent pas d'authentification
|
||||
@@ -135,38 +136,40 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
||||
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{/* Background fixe pour les images et gradients */}
|
||||
{hasCustomBackground && (
|
||||
<ImageCacheProvider>
|
||||
{/* Background fixe pour les images et gradients */}
|
||||
{hasCustomBackground && (
|
||||
<div
|
||||
className="fixed inset-0 -z-10"
|
||||
style={backgroundStyle}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="fixed inset-0 -z-10"
|
||||
style={backgroundStyle}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
|
||||
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
|
||||
>
|
||||
{!isPublicRoute && (
|
||||
<Header
|
||||
onToggleSidebar={handleToggleSidebar}
|
||||
onRefreshBackground={fetchRandomBook}
|
||||
showRefreshBackground={preferences.background.type === "komga-random"}
|
||||
/>
|
||||
)}
|
||||
{!isPublicRoute && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
initialLibraries={initialLibraries}
|
||||
initialFavorites={initialFavorites}
|
||||
userIsAdmin={userIsAdmin}
|
||||
/>
|
||||
)}
|
||||
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
|
||||
<InstallPWA />
|
||||
<Toaster />
|
||||
<NetworkStatus />
|
||||
</div>
|
||||
className={`relative min-h-screen ${hasCustomBackground ? "" : "bg-background"}`}
|
||||
style={hasCustomBackground ? { backgroundColor: `rgba(var(--background-rgb, 255, 255, 255), ${contentOpacity})` } : undefined}
|
||||
>
|
||||
{!isPublicRoute && (
|
||||
<Header
|
||||
onToggleSidebar={handleToggleSidebar}
|
||||
onRefreshBackground={fetchRandomBook}
|
||||
showRefreshBackground={preferences.background.type === "komga-random"}
|
||||
/>
|
||||
)}
|
||||
{!isPublicRoute && (
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
initialLibraries={initialLibraries}
|
||||
initialFavorites={initialFavorites}
|
||||
userIsAdmin={userIsAdmin}
|
||||
/>
|
||||
)}
|
||||
<main className={!isPublicRoute ? "pt-safe" : ""}>{children}</main>
|
||||
<InstallPWA />
|
||||
<Toaster />
|
||||
<NetworkStatus />
|
||||
</div>
|
||||
</ImageCacheProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
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 { Label } from "@/components/ui/label";
|
||||
import type { TTLConfigData } from "@/types/komga";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useImageCache } from "@/contexts/ImageCacheContext";
|
||||
|
||||
interface CacheSettingsProps {
|
||||
initialTTLConfig: TTLConfigData | null;
|
||||
@@ -35,6 +36,7 @@ interface ServiceWorkerCacheEntry {
|
||||
export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { flushImageCache } = useImageCache();
|
||||
const [isCacheClearing, setIsCacheClearing] = useState(false);
|
||||
const [isServiceWorkerClearing, setIsServiceWorkerClearing] = useState(false);
|
||||
const [serverCacheSize, setServerCacheSize] = useState<CacheSizeInfo | null>(null);
|
||||
@@ -56,6 +58,7 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
seriesTTL: 5,
|
||||
booksTTL: 5,
|
||||
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;
|
||||
setTTLConfig((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"
|
||||
/>
|
||||
</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 className="flex gap-3">
|
||||
<button
|
||||
@@ -829,6 +864,16 @@ export function CacheSettings({ initialTTLConfig }: CacheSettingsProps) {
|
||||
)}
|
||||
</button>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
import type { BookCoverProps } from "./cover-utils";
|
||||
import { getImageUrl } from "./cover-utils";
|
||||
import { useImageUrl } from "@/hooks/useImageUrl";
|
||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||
import { MarkAsReadButton } from "./mark-as-read-button";
|
||||
import { MarkAsUnreadButton } from "./mark-as-unread-button";
|
||||
@@ -59,7 +60,8 @@ export function BookCover({
|
||||
}: BookCoverProps) {
|
||||
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 currentPage = ClientOfflineBookService.getCurrentPage(book);
|
||||
|
||||
@@ -20,6 +20,10 @@ export interface SeriesCoverProps extends BaseCoverProps {
|
||||
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) {
|
||||
if (type === "series") {
|
||||
return `/api/komga/images/series/${id}/thumbnail`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CoverClient } from "./cover-client";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
import type { SeriesCoverProps } from "./cover-utils";
|
||||
import { getImageUrl } from "./cover-utils";
|
||||
import { useImageUrl } from "@/hooks/useImageUrl";
|
||||
|
||||
export function SeriesCover({
|
||||
series,
|
||||
@@ -11,7 +12,8 @@ export function SeriesCover({
|
||||
className,
|
||||
showProgressUi = true,
|
||||
}: SeriesCoverProps) {
|
||||
const imageUrl = getImageUrl("series", series.id);
|
||||
const baseUrl = getImageUrl("series", series.id);
|
||||
const imageUrl = useImageUrl(baseUrl);
|
||||
const isCompleted = series.booksCount === 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",
|
||||
"series": "Series 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": {
|
||||
"saveTTL": "Save TTL",
|
||||
"clear": "Clear cache",
|
||||
"clearing": "Clearing...",
|
||||
"clearServiceWorker": "Clear service worker cache",
|
||||
"clearingServiceWorker": "Clearing service worker cache..."
|
||||
"clearingServiceWorker": "Clearing service worker cache...",
|
||||
"flushImageCache": "Force reload images"
|
||||
},
|
||||
"messages": {
|
||||
"ttlSaved": "TTL configuration saved 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": {
|
||||
"title": "Error clearing cache",
|
||||
|
||||
@@ -145,19 +145,33 @@
|
||||
"libraries": "TTL bibliothèques",
|
||||
"series": "TTL séries",
|
||||
"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": {
|
||||
"saveTTL": "Sauvegarder les TTL",
|
||||
"clear": "Vider le cache",
|
||||
"clearing": "Suppression...",
|
||||
"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": {
|
||||
"ttlSaved": "La configuration des TTL a été sauvegardée 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": {
|
||||
"title": "Erreur lors de la suppression du cache",
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
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 { ImageService } from "./image.service";
|
||||
import { PreferencesService } from "./preferences.service";
|
||||
import { ConfigDBService } from "./config-db.service";
|
||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
import { SeriesService } from "./series.service";
|
||||
import type { Series } from "@/types/series";
|
||||
|
||||
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> {
|
||||
try {
|
||||
return this.fetchWithCache<KomgaBookWithPages>(
|
||||
@@ -102,10 +113,12 @@ export class BookService extends BaseApiService {
|
||||
response.buffer.byteOffset + response.buffer.byteLength
|
||||
) as ArrayBuffer;
|
||||
|
||||
const maxAge = await this.getImageCacheMaxAge();
|
||||
|
||||
return new Response(arrayBuffer, {
|
||||
headers: {
|
||||
"Content-Type": response.contentType || "image/jpeg",
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -117,6 +130,7 @@ export class BookService extends BaseApiService {
|
||||
try {
|
||||
// Récupérer les préférences de l'utilisateur
|
||||
const preferences = await PreferencesService.getPreferences();
|
||||
const maxAge = await this.getImageCacheMaxAge();
|
||||
|
||||
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
||||
if (preferences.showThumbnails) {
|
||||
@@ -124,7 +138,7 @@ export class BookService extends BaseApiService {
|
||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||
headers: {
|
||||
"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(
|
||||
`books/${bookId}/pages/${pageNumber}/thumbnail?zero_based=true`
|
||||
);
|
||||
const maxAge = await this.getImageCacheMaxAge();
|
||||
|
||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||
headers: {
|
||||
"Content-Type": response.contentType || "image/jpeg",
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Cache-Control": `public, max-age=${maxAge}, immutable`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -90,6 +90,7 @@ export class ConfigDBService {
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
imageCacheMaxAge: data.imageCacheMaxAge,
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
@@ -99,6 +100,7 @@ export class ConfigDBService {
|
||||
seriesTTL: data.seriesTTL,
|
||||
booksTTL: data.booksTTL,
|
||||
imagesTTL: data.imagesTTL,
|
||||
imageCacheMaxAge: data.imageCacheMaxAge,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BaseApiService } from "./base-api.service";
|
||||
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 type { ImageResponse } from "./image.service";
|
||||
import { ImageService } from "./image.service";
|
||||
import { PreferencesService } from "./preferences.service";
|
||||
import { ConfigDBService } from "./config-db.service";
|
||||
import { getServerCacheService } from "./server-cache.service";
|
||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
@@ -12,6 +13,16 @@ import type { UserPreferences } from "@/types/preferences";
|
||||
import type { ServerCacheService } from "./server-cache.service";
|
||||
|
||||
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> {
|
||||
try {
|
||||
return this.fetchWithCache<KomgaSeries>(
|
||||
@@ -179,6 +190,7 @@ export class SeriesService extends BaseApiService {
|
||||
try {
|
||||
// Récupérer les préférences de l'utilisateur
|
||||
const preferences: UserPreferences = await PreferencesService.getPreferences();
|
||||
const maxAge = await this.getImageCacheMaxAge();
|
||||
|
||||
// Si l'utilisateur préfère les vignettes, utiliser la miniature
|
||||
if (preferences.showThumbnails) {
|
||||
@@ -186,7 +198,7 @@ export class SeriesService extends BaseApiService {
|
||||
return new Response(response.buffer.buffer as ArrayBuffer, {
|
||||
headers: {
|
||||
"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;
|
||||
booksTTL: number;
|
||||
imagesTTL: number;
|
||||
imageCacheMaxAge: number; // en secondes
|
||||
}
|
||||
|
||||
export interface TTLConfig extends TTLConfigData {
|
||||
|
||||
Reference in New Issue
Block a user