diff --git a/.gitignore b/.gitignore index 17e4806..0a99797 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,9 @@ next-env.d.ts /data/*.db /data/backups/* +/data/uploads/notes/* +!/data/uploads/notes/.gitkeep -# Uploaded images +# Uploaded images (legacy - now using data/uploads) /public/uploads/notes/* !/public/uploads/notes/.gitkeep diff --git a/data/README.md b/data/README.md index ef9526d..6d1f5e8 100755 --- a/data/README.md +++ b/data/README.md @@ -9,10 +9,13 @@ data/ ├── README.md # Ce fichier ├── prod.db # Base de données production (Docker) ├── dev.db # Base de données développement (Docker) -└── backups/ # Sauvegardes automatiques et manuelles - ├── towercontrol_2025-01-15T10-30-00-000Z.db.gz - ├── towercontrol_2025-01-15T11-30-00-000Z.db.gz - └── ... +├── backups/ # Sauvegardes automatiques et manuelles +│ ├── towercontrol_2025-01-15T10-30-00-000Z.db.gz +│ ├── towercontrol_2025-01-15T11-30-00-000Z.db.gz +│ └── ... +└── uploads/ # Images et fichiers uploadés + └── notes/ # Images des notes markdown + └── ... ``` ## 🎯 Utilisation @@ -60,6 +63,13 @@ BACKUP_STORAGE_PATH="./data/backups" - **Rétention** : Configurable (défaut: 5 sauvegardes) - **Fréquence** : Configurable (défaut: horaire) +### Images uploadées + +- **Dossier** : `data/uploads/notes/` +- **Format** : Images collées dans les notes markdown +- **Accès** : Via `/api/notes/images/[filename]` +- **Persistance** : Monté en volume Docker pour persistance + ## 🚀 Commandes utiles ```bash diff --git a/data/uploads/notes/.gitkeep b/data/uploads/notes/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/src/app/api/notes/images/[filename]/route.ts b/src/app/api/notes/images/[filename]/route.ts new file mode 100644 index 0000000..27626b6 --- /dev/null +++ b/src/app/api/notes/images/[filename]/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import { existsSync } from 'fs'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { filename } = await params; + + // Sécurité : empêcher les path traversal attacks + if (filename.includes('..') || filename.includes('/')) { + return NextResponse.json( + { error: 'Nom de fichier invalide' }, + { status: 400 } + ); + } + + // Chemin vers l'image dans data/uploads/notes + const imagePath = join(process.cwd(), 'data', 'uploads', 'notes', filename); + + // Vérifier que le fichier existe + if (!existsSync(imagePath)) { + return NextResponse.json({ error: 'Image non trouvée' }, { status: 404 }); + } + + // Lire le fichier + const imageBuffer = await readFile(imagePath); + + // Déterminer le type MIME basé sur l'extension + const extension = filename.split('.').pop()?.toLowerCase(); + const mimeTypes: Record = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + }; + const contentType = mimeTypes[extension || ''] || 'image/jpeg'; + + // Retourner l'image avec les bons headers + return new NextResponse(new Uint8Array(imageBuffer), { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }); + } catch (error) { + console.error('Error serving image:', error); + return NextResponse.json( + { error: "Erreur lors de la récupération de l'image" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/notes/images/upload/route.ts b/src/app/api/notes/images/upload/route.ts index ffb7403..9a21d55 100644 --- a/src/app/api/notes/images/upload/route.ts +++ b/src/app/api/notes/images/upload/route.ts @@ -40,7 +40,8 @@ export async function POST(request: NextRequest) { } // Créer le dossier de stockage s'il n'existe pas - const uploadsDir = join(process.cwd(), 'public', 'uploads', 'notes'); + // Utiliser data/ qui est monté en volume dans Docker + const uploadsDir = join(process.cwd(), 'data', 'uploads', 'notes'); if (!existsSync(uploadsDir)) { await mkdir(uploadsDir, { recursive: true }); } @@ -58,7 +59,7 @@ export async function POST(request: NextRequest) { await writeFile(filepath, buffer); // Retourner l'URL relative de l'image - const imageUrl = `/uploads/notes/${filename}`; + const imageUrl = `/api/notes/images/${filename}`; return NextResponse.json({ url: imageUrl }); } catch (error) {