Persist media uploads outside public/ to survive rebuilds and redeploys
Local Build and Deploy / deploy (push) Successful in 1m9s
Local Build and Deploy / deploy (push) Successful in 1m9s
Move media storage from public/assets/images/media to storage/media (outside Git and public/), so uploaded images, favicons and logos are preserved across rebuilds and git clean. Add a shared media-storage helper, update the upload/serve actions and API routes, and gitignore storage/.
This commit is contained in:
1 parent
0d82f1325e
commit
1bbbf6e5a4
693 files changed
+38
-19
No files matched your search
@@ -5,8 +5,8 @@ import path from "node:path";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { MEDIA_ROOT, resolveMediaPath } from "@/lib/media-storage";
|
||||
|
||||
const MEDIA_DIR = "public/assets/images/media";
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ALLOWED = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
|
||||
@@ -17,14 +17,14 @@ export async function uploadMedia(formData: FormData): Promise<void> {
|
||||
if (file.size > MAX_SIZE) return;
|
||||
if (!ALLOWED.includes(file.type)) return;
|
||||
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const baseDir = MEDIA_ROOT;
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
await mkdir(baseDir, { recursive: true });
|
||||
|
||||
const ext = file.name.split(".").pop() ?? "png";
|
||||
const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
||||
const bytes = await file.arrayBuffer();
|
||||
const filePath = path.resolve(baseDir, name);
|
||||
const filePath = resolveMediaPath(name);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) throw new Error("Invalid path");
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
await writeFile(filePath, Buffer.from(bytes));
|
||||
@@ -36,8 +36,8 @@ export async function uploadMedia(formData: FormData): Promise<void> {
|
||||
export async function deleteMedia(name: string): Promise<void> {
|
||||
await requirePermission(PERMS.PAGES_EDIT);
|
||||
const { unlink } = await import("node:fs/promises");
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const filePath = path.resolve(baseDir, name);
|
||||
const baseDir = MEDIA_ROOT;
|
||||
const filePath = resolveMediaPath(name);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) return;
|
||||
try {
|
||||
await unlink(filePath);
|
||||
@@ -57,14 +57,14 @@ export async function uploadMediaAndReturn(
|
||||
if (file.size > MAX_SIZE) return "";
|
||||
if (!ALLOWED.includes(file.type)) return "";
|
||||
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const baseDir = MEDIA_ROOT;
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
await mkdir(baseDir, { recursive: true });
|
||||
|
||||
const ext = file.name.split(".").pop() ?? "png";
|
||||
const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
||||
const bytes = await file.arrayBuffer();
|
||||
const filePath = path.resolve(baseDir, name);
|
||||
const filePath = resolveMediaPath(name);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) return "";
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
await writeFile(filePath, Buffer.from(bytes));
|
||||
|
||||
@@ -5,8 +5,9 @@ import path from "node:path";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { resolveMediaPath } from "@/lib/media-storage";
|
||||
|
||||
const FAVICON_DIR = "public/assets/images/media/favicon";
|
||||
const FAVICON_DIR = resolveMediaPath("favicon");
|
||||
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
|
||||
const ALLOWED = [
|
||||
"image/png",
|
||||
@@ -42,7 +43,7 @@ export async function saveFavicon(
|
||||
};
|
||||
const ext = mimeExt[file.type] ?? "png";
|
||||
const filename = `favicon-${Date.now()}.${ext}`;
|
||||
const baseDir = path.resolve(process.cwd(), FAVICON_DIR);
|
||||
const baseDir = FAVICON_DIR;
|
||||
const filePath = path.resolve(baseDir, filename);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) {
|
||||
return { success: false, error: "Invalid path" };
|
||||
@@ -99,7 +100,7 @@ export async function deleteFavicon(): Promise<{
|
||||
try {
|
||||
const oldUrl = await siteSettings.get("cms_favicon");
|
||||
if (oldUrl?.startsWith("/api/media/favicon/")) {
|
||||
const baseDir = path.resolve(process.cwd(), FAVICON_DIR);
|
||||
const baseDir = FAVICON_DIR;
|
||||
const oldName = oldUrl.replace("/api/media/favicon/", "");
|
||||
if (!oldName.includes("..") && !oldName.includes("/")) {
|
||||
const oldPath = path.resolve(baseDir, oldName);
|
||||
|
||||
@@ -5,8 +5,9 @@ import path from "node:path";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { resolveMediaPath } from "@/lib/media-storage";
|
||||
|
||||
const MEDIA_DIR = "public/assets/images/media/logo";
|
||||
const MEDIA_DIR = resolveMediaPath("logo");
|
||||
|
||||
export async function saveLogo(
|
||||
formData: FormData,
|
||||
@@ -26,7 +27,7 @@ export async function saveLogo(
|
||||
? "webp"
|
||||
: "png";
|
||||
const filename = `logo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const baseDir = MEDIA_DIR;
|
||||
const filePath = path.resolve(baseDir, filename);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) {
|
||||
return { success: false, error: "Invalid path" };
|
||||
@@ -80,7 +81,7 @@ export async function saveLogoFromUrl(
|
||||
? "webp"
|
||||
: "gif";
|
||||
const filename = `logo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const baseDir = MEDIA_DIR;
|
||||
const filePath = path.resolve(baseDir, filename);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) {
|
||||
return { success: false, error: "Invalid path" };
|
||||
|
||||
@@ -2,10 +2,10 @@ import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { NextResponse } from "next/server";
|
||||
import { MEDIA_ROOT, resolveMediaPath } from "@/lib/media-storage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const MEDIA_DIR = "public/assets/images/media";
|
||||
const ALLOWED_EXT = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"];
|
||||
|
||||
export async function GET(
|
||||
@@ -23,8 +23,8 @@ export async function GET(
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
|
||||
const filePath = path.resolve(baseDir, name);
|
||||
const baseDir = MEDIA_ROOT;
|
||||
const filePath = resolveMediaPath(name);
|
||||
if (!filePath.startsWith(baseDir + path.sep)) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { NextResponse } from "next/server";
|
||||
import { MEDIA_ROOT } from "@/lib/media-storage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const MEDIA_DIR = "assets/images/media";
|
||||
|
||||
export async function GET() {
|
||||
const dir = path.resolve(process.cwd(), "public", MEDIA_DIR);
|
||||
const dir = MEDIA_ROOT;
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
if (!existsSync(dir)) {
|
||||
return NextResponse.json({ files: [] });
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Persistent media root. Lives outside `public/` and outside Git so uploads
|
||||
* survive rebuilds, redeploys and `git clean`. Served through /api/media.
|
||||
*/
|
||||
export const MEDIA_ROOT = path.resolve(process.cwd(), "storage", "media");
|
||||
|
||||
export function resolveMediaPath(...segments: string[]): string {
|
||||
const target = path.resolve(MEDIA_ROOT, ...segments);
|
||||
if (target !== MEDIA_ROOT && !target.startsWith(MEDIA_ROOT + path.sep)) {
|
||||
throw new Error("Invalid media path");
|
||||
}
|
||||
return target;
|
||||
}
|
||||
Reference in new issue
Block a user