diff --git a/memento-note/Dockerfile b/memento-note/Dockerfile index b8c9fd1..19bc325 100644 --- a/memento-note/Dockerfile +++ b/memento-note/Dockerfile @@ -47,7 +47,10 @@ RUN groupadd --system --gid 1001 nodejs RUN useradd --system --uid 1001 --gid nodejs nextjs # Static assets -COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/public ./public + +# Upload directory (outside public/ — served via API route) +RUN mkdir -p ./data/uploads/notes && chown -R nextjs:nodejs ./data # Next.js standalone output COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ diff --git a/memento-note/app/api/chat/route.ts b/memento-note/app/api/chat/route.ts index f160bfa..9d46b7d 100644 --- a/memento-note/app/api/chat/route.ts +++ b/memento-note/app/api/chat/route.ts @@ -49,7 +49,7 @@ export async function POST(req: Request) { // 2. Parse request body — messages arrive as UIMessage[] from DefaultChatTransport const body = await req.json() - console.log('[Chat] body keys:', Object.keys(body), 'noteContext?', !!body.noteContext, 'images?', body.noteContext?.images?.length) + const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext } = body as { messages: UIMessage[] conversationId?: string @@ -331,22 +331,16 @@ Tu as accès à ces outils pour des recherches approfondies : // Load note images as base64 for vision-capable models let imageContextParts: Array<{ type: 'image'; image: string }> = [] if (noteContext?.images && noteContext.images.length > 0) { - console.log('[Chat] noteContext.images:', noteContext.images) for (const imgPath of noteContext.images.slice(0, 4)) { try { - const fullPath = path.join(process.cwd(), 'public', imgPath) - console.log('[Chat] reading image:', fullPath) + const fullPath = path.join(process.cwd(), 'data', imgPath) const buffer = await readFile(fullPath) const ext = path.extname(imgPath).toLowerCase() const mime = ext === '.png' ? 'image/png' : ext === '.gif' ? 'image/gif' : ext === '.webp' ? 'image/webp' : 'image/jpeg' const base64 = `data:${mime};base64,${buffer.toString('base64')}` imageContextParts.push({ type: 'image', image: base64 }) - console.log('[Chat] image loaded, size:', buffer.length, 'bytes') - } catch (err) { - console.error('[Chat] failed to read image:', imgPath, err) - } + } catch {} } - console.log('[Chat] total image parts:', imageContextParts.length) } let copilotContext = '' diff --git a/memento-note/app/api/upload/route.ts b/memento-note/app/api/upload/route.ts index 463577b..1f9cbae 100644 --- a/memento-note/app/api/upload/route.ts +++ b/memento-note/app/api/upload/route.ts @@ -46,8 +46,8 @@ export async function POST(request: NextRequest) { } const filename = `${randomUUID()}${ext}` - // Ensure directory exists - const uploadDir = path.join(process.cwd(), 'public/uploads/notes') + // Ensure directory exists (data/uploads is outside public/, served via API route) + const uploadDir = path.join(process.cwd(), 'data/uploads/notes') await mkdir(uploadDir, { recursive: true }) const filePath = path.join(uploadDir, filename) diff --git a/memento-note/app/api/uploads/[...path]/route.ts b/memento-note/app/api/uploads/[...path]/route.ts new file mode 100644 index 0000000..a948a78 --- /dev/null +++ b/memento-note/app/api/uploads/[...path]/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from 'next/server' +import { readFile, stat } from 'fs/promises' +import path from 'path' +import { auth } from '@/auth' + +const UPLOAD_DIR = path.join(process.cwd(), 'data', 'uploads') + +const MIME_MAP: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', +} + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + const { path: segments } = await params + + // Only serve from uploads/notes/ subdirectory + if (segments[0] !== 'notes') { + return new NextResponse('Not found', { status: 404 }) + } + + const filename = segments[segments.length - 1] + const ext = path.extname(filename).toLowerCase() + const contentType = MIME_MAP[ext] + if (!contentType) { + return new NextResponse('Unsupported file type', { status: 400 }) + } + + // Prevent path traversal + const safePath = path.join(UPLOAD_DIR, ...segments) + if (!safePath.startsWith(UPLOAD_DIR)) { + return new NextResponse('Forbidden', { status: 403 }) + } + + try { + const fileStats = await stat(safePath) + if (!fileStats.isFile()) { + return new NextResponse('Not found', { status: 404 }) + } + + const buffer = await readFile(safePath) + + return new NextResponse(buffer, { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=31536000, immutable', + 'Content-Length': String(buffer.length), + }, + }) + } catch { + return new NextResponse('Not found', { status: 404 }) + } +} diff --git a/memento-note/docker-compose.yml b/memento-note/docker-compose.yml index 890f2f2..7d4f88a 100644 --- a/memento-note/docker-compose.yml +++ b/memento-note/docker-compose.yml @@ -50,7 +50,7 @@ services: # - OLLAMA_MODEL=granite4:latest volumes: # Persist uploaded images and files - - keep-uploads:/app/public/uploads + - keep-uploads:/app/data/uploads # Optional: Mount custom SSL certificates # - ./certs:/app/certs:ro diff --git a/memento-note/lib/ai/tools/extract-images.ts b/memento-note/lib/ai/tools/extract-images.ts index 82c9ee3..ca8ac7f 100644 --- a/memento-note/lib/ai/tools/extract-images.ts +++ b/memento-note/lib/ai/tools/extract-images.ts @@ -10,7 +10,7 @@ import path from 'path' import { randomUUID } from 'crypto' import sharp from 'sharp' -const UPLOADS_DIR = 'public/uploads/notes' +const UPLOADS_DIR = 'data/uploads/notes' const URL_PREFIX = '/uploads/notes' const MAX_IMAGES_PER_PAGE = 3 const MIN_IMAGE_SIZE = 200 // px -- skip icons, spacers, tracking pixels diff --git a/memento-note/lib/image-cleanup.ts b/memento-note/lib/image-cleanup.ts index c63a8d1..890fc3e 100644 --- a/memento-note/lib/image-cleanup.ts +++ b/memento-note/lib/image-cleanup.ts @@ -8,7 +8,7 @@ import { promises as fs } from 'fs' import path from 'path' import { prisma } from '@/lib/prisma' -const UPLOADS_DIR = 'public/uploads/notes' +const UPLOADS_DIR = 'data/uploads/notes' /** * Delete an image file from disk only if no other note references it. @@ -26,7 +26,7 @@ export async function deleteImageFileSafely(imageUrl: string, excludeNoteId?: st const otherRefs = notes.filter(n => n.id !== excludeNoteId) if (otherRefs.length > 0) return // File still referenced elsewhere - const filePath = path.join(process.cwd(), imageUrl) + const filePath = path.join(process.cwd(), 'data', imageUrl) await fs.unlink(filePath) } catch { // File already gone or unreadable -- silently skip diff --git a/memento-note/next.config.ts b/memento-note/next.config.ts index 7fe68ea..b2f9164 100644 --- a/memento-note/next.config.ts +++ b/memento-note/next.config.ts @@ -4,6 +4,16 @@ const nextConfig: NextConfig = { // Enable standalone output for Docker output: 'standalone', + // Serve dynamically uploaded files via API route (public/ is read-only in production) + async rewrites() { + return [ + { + source: '/uploads/:path*', + destination: '/api/uploads/:path*', + }, + ] + }, + // Image optimization (enabled for better performance) images: { formats: ['image/avif', 'image/webp'],