mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 05:07:46 +00:00
feat: move images to nginx
This commit is contained in:
parent
a42a4126ec
commit
c951c7d755
11 changed files with 29 additions and 169 deletions
|
|
@ -5,7 +5,8 @@ REDIS_URL="redis://localhost:6379/0"
|
|||
|
||||
# Used for metadata, sitemaps, etc.
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
FRONTEND_URL=http://localhost:4321
|
||||
NEXT_PUBLIC_FRONTEND_URL=http://localhost:5173
|
||||
NEXT_PUBLIC_STATIC_URL=
|
||||
|
||||
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
||||
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX
|
||||
|
|
|
|||
|
|
@ -246,8 +246,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||
body: JSON.stringify({
|
||||
files: [
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}`,
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=mii`,
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=features`,
|
||||
`${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/mii.png`,
|
||||
`${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/features.png`,
|
||||
],
|
||||
}),
|
||||
}).catch((err) => {
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { generateMetadataImage } from "@/lib/images";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
type: z
|
||||
.enum(["mii", "qr-code", "features", "image0", "image1", "image2", "metadata"], {
|
||||
message: "Image type must be either 'mii', 'qr-code', 'features', 'image[number from 0 to 2]' or 'metadata'",
|
||||
})
|
||||
.default("mii"),
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const rateLimit = new RateLimit(request, 200, "/mii/image");
|
||||
const check = await rateLimit.handle();
|
||||
if (check) return check;
|
||||
|
||||
const { id: slugId } = await params;
|
||||
const parsed = idSchema.safeParse(slugId);
|
||||
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||
const miiId = parsed.data;
|
||||
|
||||
const searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
||||
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
|
||||
const { type: imageType } = searchParamsParsed.data;
|
||||
|
||||
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.png`);
|
||||
|
||||
let buffer: Buffer | undefined;
|
||||
// Only find Mii if image type is 'metadata'
|
||||
let mii: Prisma.MiiGetPayload<{
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}> | null = null;
|
||||
|
||||
if (imageType === "metadata") {
|
||||
mii = await prisma.mii.findUnique({
|
||||
where: {
|
||||
id: miiId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!mii) {
|
||||
return rateLimit.sendResponse({ error: "Mii not found" }, 404);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to read file
|
||||
buffer = await fs.readFile(filePath);
|
||||
} catch {
|
||||
// If the readFile() fails, that probably means it doesn't exist
|
||||
if (imageType === "metadata" && mii) {
|
||||
// Metadata images were added after 1274 Miis were submitted, so we generate it on-the-fly
|
||||
console.log(`Metadata image not found for mii ID ${miiId}, generating metadata image...`);
|
||||
const { buffer: metadataBuffer, error, status } = await generateMetadataImage(mii, mii.user.name!);
|
||||
|
||||
if (error) {
|
||||
return rateLimit.sendResponse({ error }, status);
|
||||
}
|
||||
|
||||
buffer = metadataBuffer;
|
||||
} else {
|
||||
return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer) return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||
|
||||
// Set the file name for the metadata image in the response for SEO
|
||||
if (mii && imageType === "metadata") {
|
||||
const slugify = (str: string) =>
|
||||
str
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-") // replace non-alphanumeric with hyphens
|
||||
.replace(/^-+|-+$/g, "");
|
||||
|
||||
const name = slugify(mii.name);
|
||||
const tags = mii.tags.map(slugify).join("-");
|
||||
|
||||
const filename = `${name}-mii-${tags}.png`;
|
||||
|
||||
return rateLimit.sendResponse(buffer, 200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Disposition": `inline; filename="${filename}"`,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
});
|
||||
}
|
||||
|
||||
return rateLimit.sendResponse(buffer, 200, {
|
||||
"Content-Type": "image/png",
|
||||
"X-Robots-Tag": "noindex, noimageindex, nofollow",
|
||||
"Cache-Control": "public, max-age=60, stale-while-revalidate=30",
|
||||
});
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const rateLimit = new RateLimit(request, 16, "/profile/picture");
|
||||
const check = await rateLimit.handle();
|
||||
if (check) return check;
|
||||
|
||||
const { id: slugId } = await params;
|
||||
const parsed = idSchema.safeParse(slugId);
|
||||
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||
const userId = parsed.data;
|
||||
|
||||
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.png`);
|
||||
|
||||
try {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
return new NextResponse(new Uint8Array(buffer)); // convert to Uint8Array due to weird types issue
|
||||
} catch {
|
||||
return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,9 @@ export const revalidate = 43200; // update every 12 hours
|
|||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
||||
if (!baseUrl) {
|
||||
console.error("NEXT_PUBLIC_BASE_URL environment variable missing");
|
||||
const staticUrl = process.env.NEXT_PUBLIC_STATIC_URL;
|
||||
if (!baseUrl || !staticUrl) {
|
||||
console.error("NEXT_PUBLIC_BASE_URL or NEXT_PUBLIC_STATIC_URL environment variable missing");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||
lastModified: mii.createdAt,
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.7,
|
||||
images: [`${baseUrl}/mii/${mii.id}/image?type=metadata`],
|
||||
images: [`${staticUrl}/mii/${mii.id}/metadata.png`],
|
||||
}) as SitemapRoute,
|
||||
),
|
||||
...users.map(
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr
|
|||
<p className="text-sm text-zinc-500">Are you sure? This will delete your Mii permanently. This action cannot be undone.</p>
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 flex overflow-hidden">
|
||||
<img src={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=mii`} alt="mii image" width={128} height={128} />
|
||||
<img src={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/mii.png`} alt="mii image" width={128} height={128} />
|
||||
<div className="p-4 min-w-0">
|
||||
<p className="text-xl font-bold line-clamp-3 wrap-anywhere" title={miiName}>
|
||||
{miiName}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default function MiiList({ parentPage, userId }: Props) {
|
|||
{parentPage !== "admin" ? (
|
||||
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`}
|
||||
src={`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`}
|
||||
width={240}
|
||||
height={160}
|
||||
alt="mii image"
|
||||
|
|
@ -136,11 +136,11 @@ export default function MiiList({ parentPage, userId }: Props) {
|
|||
) : (
|
||||
<div className="grid grid-cols-2 gap-1 rounded-xl bg-zinc-200">
|
||||
{[
|
||||
`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`,
|
||||
`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`,
|
||||
mii.platform === "THREE_DS"
|
||||
? `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=qr-code`
|
||||
: `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=features`,
|
||||
...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${i}`),
|
||||
? `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/qr-code.png`
|
||||
: `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/features.png`,
|
||||
...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/image${i}.png`),
|
||||
].map((src, i) => (
|
||||
<img key={i} src={src} alt="mii image" className="w-full bg-zinc-200" />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
|||
};
|
||||
|
||||
const handleCopyImage = async () => {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`);
|
||||
const response = await fetch(`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`);
|
||||
const blob = await response.blob();
|
||||
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
|
|
@ -118,7 +118,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
|||
|
||||
<div className="flex justify-center items-center p-4 w-full bg-orange-100 border border-orange-400 rounded-lg">
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`}
|
||||
src={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`}
|
||||
alt="mii 'metadata' image"
|
||||
width={248}
|
||||
height={248}
|
||||
|
|
@ -130,7 +130,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
|||
<div className="flex gap-2 w-full">
|
||||
{/* Save button */}
|
||||
<Link
|
||||
to={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`}
|
||||
to={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`}
|
||||
className="pill button p-0! aspect-square size-11 cursor-pointer text-xl"
|
||||
aria-label="Save Image"
|
||||
data-tooltip="Save Image"
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export default function EditMiiPage() {
|
|||
try {
|
||||
const existing = await Promise.all(
|
||||
Array.from({ length: mii.imageCount }, async (_, index) => {
|
||||
const path = `${API_URL}/mii/${mii.id}/image?type=image${index}`;
|
||||
const path = `${STATIC_URL}/mii/${mii.id}/image${index}.png`;
|
||||
const response = await fetch(path);
|
||||
const blob = await response.blob();
|
||||
|
||||
|
|
@ -167,6 +167,7 @@ export default function EditMiiPage() {
|
|||
}, [mii, mii?.id, mii?.imageCount]);
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const STATIC_URL = import.meta.env.VITE_STATIC_URL;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/mii/${id}/info`)
|
||||
|
|
@ -181,8 +182,8 @@ export default function EditMiiPage() {
|
|||
setDescription(data.description);
|
||||
setGender(data.gender ?? "MALE");
|
||||
setMakeup(data.makeup ?? "PARTIAL");
|
||||
setMiiPortraitUri(`${API_URL}/mii/${data.id}/image?type=mii`);
|
||||
setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`);
|
||||
setMiiPortraitUri(`${STATIC_URL}/mii/${data.id}/mii.png`);
|
||||
setMiiFeaturesUri(`${STATIC_URL}/mii/${data.id}/features.png`);
|
||||
setYouTubeId(data.youtubeId ?? "");
|
||||
setQuarantined(data.quarantined);
|
||||
instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {});
|
||||
|
|
@ -208,10 +209,8 @@ export default function EditMiiPage() {
|
|||
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
||||
<Carousel
|
||||
images={[
|
||||
miiPortraitUri ?? `${API_URL}/mii/${mii.id}/image?type=mii`,
|
||||
...(mii.platform === "THREE_DS"
|
||||
? [`${API_URL}/mii/${mii.id}/image?type=qr-code`]
|
||||
: [miiFeaturesUri ?? `${API_URL}/mii/${mii.id}/image?type=features`]),
|
||||
miiPortraitUri ?? `${STATIC_URL}/mii/${mii.id}/mii.png`,
|
||||
...(mii.platform === "THREE_DS" ? [`${STATIC_URL}/mii/${mii.id}/qr-code.png`] : [miiFeaturesUri ?? `${STATIC_URL}/mii/${mii.id}/features.png`]),
|
||||
...files.map((file) => URL.createObjectURL(file)),
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function MiiPage() {
|
|||
const [isLiked, setIsLiked] = useState(false);
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const STATIC_URL = import.meta.env.VITE_STATIC_URL;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/mii/${id}/info`)
|
||||
|
|
@ -50,7 +51,7 @@ export default function MiiPage() {
|
|||
}, [id]);
|
||||
|
||||
if (loading || !mii) return <div className="p-6 text-center">Loading...</div>;
|
||||
const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${API_URL}/mii/${mii.id}/image?type=image${index}`)];
|
||||
const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${STATIC_URL}/mii/${mii.id}/image${index}.png`)];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
|
|
@ -76,7 +77,7 @@ export default function MiiPage() {
|
|||
{/* Mii Image */}
|
||||
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center">
|
||||
<ImageViewer
|
||||
src={`${API_URL}/mii/${mii.id}/image?type=mii`}
|
||||
src={`${STATIC_URL}/mii/${mii.id}/mii.png`}
|
||||
alt="mii headshot"
|
||||
width={250}
|
||||
height={250}
|
||||
|
|
@ -87,7 +88,7 @@ export default function MiiPage() {
|
|||
{mii.platform === "THREE_DS" ? (
|
||||
<div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2">
|
||||
<ImageViewer
|
||||
src={`${API_URL}/mii/${mii.id}/image?type=qr-code`}
|
||||
src={`${STATIC_URL}/mii/${mii.id}/qr-code.png`}
|
||||
alt="mii qr code"
|
||||
width={128}
|
||||
height={128}
|
||||
|
|
@ -96,7 +97,7 @@ export default function MiiPage() {
|
|||
</div>
|
||||
) : (
|
||||
<ImageViewer
|
||||
src={`${API_URL}/mii/${mii.id}/image?type=features`}
|
||||
src={`${STATIC_URL}/mii/${mii.id}/features.png`}
|
||||
alt="mii features"
|
||||
width={300}
|
||||
height={300}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default function ReportMiiPage() {
|
|||
<hr className="border-zinc-300" />
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex">
|
||||
<img src={`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`} alt="mii image" width={128} height={128} />
|
||||
<img src={`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`} alt="mii image" width={128} height={128} />
|
||||
<div className="p-4">
|
||||
<p className="text-xl font-bold line-clamp-1">{mii.name}</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue