fix: upload permissions in Docker and improve file validation
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-4
@@ -1,5 +1,5 @@
|
||||
FROM node:22-alpine AS base
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
RUN apk add --no-cache libc6-compat openssl su-exec
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
@@ -18,7 +18,7 @@ ENV NODE_ENV=production
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN mkdir -p public/uploads && chown nextjs:nodejs public/uploads
|
||||
RUN mkdir -p public/uploads
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next ./.next
|
||||
@@ -26,10 +26,11 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/next.config.ts ./next.config.ts
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh && chown -R nextjs:nodejs /app
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["sh", "-c", "npx prisma db push --skip-generate && npm start"]
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
mkdir -p /app/public/uploads
|
||||
chown -R nextjs:nodejs /app/public/uploads
|
||||
|
||||
cd /app
|
||||
|
||||
exec su-exec nextjs sh -c "npx prisma db push --skip-generate && exec npm start"
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "10mb",
|
||||
},
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
+62
-14
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import { writeFile, mkdir, access } from "fs/promises";
|
||||
import { constants } from "fs";
|
||||
import path from "path";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
@@ -10,7 +11,44 @@ const ALLOWED_TYPES = [
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
];
|
||||
|
||||
const MIME_BY_EXT: Record<string, string> = {
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
png: "image/png",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
svg: "image/svg+xml",
|
||||
};
|
||||
|
||||
const MAX_SIZE = 10 * 1024 * 1024;
|
||||
const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads");
|
||||
|
||||
function resolveMimeType(file: File): string | null {
|
||||
if (file.type && ALLOWED_TYPES.includes(file.type)) {
|
||||
return file.type;
|
||||
}
|
||||
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
return MIME_BY_EXT[ext] ?? null;
|
||||
}
|
||||
|
||||
function safeExtension(filename: string, mimeType: string): string {
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
if (ext && MIME_BY_EXT[ext]) {
|
||||
return ext;
|
||||
}
|
||||
|
||||
const byMime: Record<string, string> = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/gif": "gif",
|
||||
"image/webp": "webp",
|
||||
"image/svg+xml": "svg",
|
||||
};
|
||||
|
||||
return byMime[mimeType] || "jpg";
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -18,13 +56,11 @@ export async function POST(request: NextRequest) {
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: "Файл не найден" },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: "Файл не найден" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
const mimeType = resolveMimeType(file);
|
||||
if (!mimeType) {
|
||||
return NextResponse.json(
|
||||
{ error: "Недопустимый тип файла" },
|
||||
{ status: 400 }
|
||||
@@ -38,15 +74,24 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "jpg";
|
||||
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`;
|
||||
const uploadDir = path.join(process.cwd(), "public", "uploads");
|
||||
await mkdir(UPLOAD_DIR, { recursive: true });
|
||||
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
try {
|
||||
await access(UPLOAD_DIR, constants.W_OK);
|
||||
} catch {
|
||||
console.error("Upload dir not writable:", UPLOAD_DIR);
|
||||
return NextResponse.json(
|
||||
{ error: "Каталог загрузок недоступен для записи" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const ext = safeExtension(file.name, mimeType);
|
||||
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`;
|
||||
const filepath = path.join(UPLOAD_DIR, filename);
|
||||
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
await writeFile(path.join(uploadDir, filename), buffer);
|
||||
await writeFile(filepath, Buffer.from(bytes));
|
||||
|
||||
const url = `/uploads/${filename}`;
|
||||
|
||||
@@ -54,13 +99,16 @@ export async function POST(request: NextRequest) {
|
||||
data: {
|
||||
filename,
|
||||
originalName: file.name,
|
||||
mimeType: file.type,
|
||||
mimeType,
|
||||
size: file.size,
|
||||
url,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(photo);
|
||||
return NextResponse.json({
|
||||
...photo,
|
||||
createdAt: photo.createdAt.toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
Reference in New Issue
Block a user