fix: move mii images to new uploads directory and add route to access it

This commit is contained in:
trafficlunar 2025-04-30 19:25:50 +01:00
parent 594309d22d
commit eea3df283c
12 changed files with 73 additions and 33 deletions

3
.gitignore vendored
View file

@ -41,4 +41,5 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
public/mii/ # tomodachi-share
uploads/

View file

@ -20,7 +20,6 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
const miiId = parsed.data; const miiId = parsed.data;

View file

@ -35,7 +35,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
// Get Mii ID // Get Mii ID
const { id: slugId } = await params; const { id: slugId } = await params;
const parsedId = idSchema.safeParse(slugId); const parsedId = idSchema.safeParse(slugId);
if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.errors[0].message }, 400); if (!parsedId.success) return rateLimit.sendResponse({ error: parsedId.error.errors[0].message }, 400);
const miiId = parsedId.data; const miiId = parsedId.data;

View file

@ -15,7 +15,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const { id: slugId } = await params; const { id: slugId } = await params;
const parsed = idSchema.safeParse(slugId); const parsed = idSchema.safeParse(slugId);
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400);
const miiId = parsed.data; const miiId = parsed.data;

View file

@ -18,7 +18,7 @@ import { convertQrCode } from "@/lib/qr-codes";
import Mii from "@/lib/mii.js/mii"; import Mii from "@/lib/mii.js/mii";
import TomodachiLifeMii from "@/lib/tomodachi-life-mii"; import TomodachiLifeMii from "@/lib/tomodachi-life-mii";
const uploadsDirectory = path.join(process.cwd(), "public", "mii"); const uploadsDirectory = path.join(process.cwd(), "uploads");
const submitSchema = z.object({ const submitSchema = z.object({
name: nameSchema, name: nameSchema,

View file

@ -6,15 +6,15 @@ import { prisma } from "@/lib/prisma";
import EditForm from "@/components/submit-form/edit-form"; import EditForm from "@/components/submit-form/edit-form";
interface Props { interface Props {
params: Promise<{ slug: string }>; params: Promise<{ id: string }>;
} }
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params; const { id } = await params;
const mii = await prisma.mii.findUnique({ const mii = await prisma.mii.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
}); });
@ -29,12 +29,12 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
export default async function MiiPage({ params }: Props) { export default async function MiiPage({ params }: Props) {
const { slug } = await params; const { id } = await params;
const session = await auth(); const session = await auth();
const mii = await prisma.mii.findUnique({ const mii = await prisma.mii.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
include: { include: {
_count: { _count: {

View file

@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
import { idSchema } from "@/lib/schemas";
import { RateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
type: z
.enum(["mii", "qr-code", "image0", "image1", "image2"], {
message: "Image type must be either 'mii', 'qr-code' or 'image[number from 0 to 2]'",
})
.default("mii"),
});
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const rateLimit = new RateLimit(request, 200);
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.errors[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.errors[0].message }, 400);
const { type: imageType } = searchParamsParsed.data;
const filePath = path.join(process.cwd(), "uploads", miiId.toString(), `${imageType}.webp`);
try {
const buffer = await fs.readFile(filePath);
return new NextResponse(buffer);
} catch {
return rateLimit.sendResponse({ success: false, error: "Image not found" }, 404);
}
}

View file

@ -14,15 +14,15 @@ import DeleteMiiButton from "@/components/delete-mii";
import ScanTutorialButton from "@/components/tutorial/scan"; import ScanTutorialButton from "@/components/tutorial/scan";
interface Props { interface Props {
params: Promise<{ slug: string }>; params: Promise<{ id: string }>;
} }
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params; const { id } = await params;
const mii = await prisma.mii.findUnique({ const mii = await prisma.mii.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
include: { include: {
user: { user: {
@ -39,8 +39,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Bots get redirected anyways // Bots get redirected anyways
if (!mii) return {}; if (!mii) return {};
const miiImageUrl = `/mii/${mii.id}/mii.webp`; const miiImageUrl = `/mii/${mii.id}/image?type=mii`;
const qrCodeUrl = `/mii/${mii.id}/qrcode.webp`; const qrCodeUrl = `/mii/${mii.id}/image?type=qr-code`;
const username = `@${mii.user.username}`; const username = `@${mii.user.username}`;
@ -73,12 +73,12 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
export default async function MiiPage({ params }: Props) { export default async function MiiPage({ params }: Props) {
const { slug } = await params; const { id } = await params;
const session = await auth(); const session = await auth();
const mii = await prisma.mii.findUnique({ const mii = await prisma.mii.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
include: { include: {
user: { user: {
@ -103,9 +103,9 @@ export default async function MiiPage({ params }: Props) {
if (!mii) redirect("/404"); if (!mii) redirect("/404");
const images = [ const images = [
`/mii/${mii.id}/mii.webp`, `/mii/${mii.id}/image?type=mii`,
`/mii/${mii.id}/qr-code.webp`, `/mii/${mii.id}/image?type=qr-code`,
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image${index}.webp`), ...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
]; ];
return ( return (

View file

@ -11,15 +11,15 @@ import { prisma } from "@/lib/prisma";
import MiiList from "@/components/mii-list"; import MiiList from "@/components/mii-list";
interface Props { interface Props {
params: Promise<{ slug: string }>; params: Promise<{ id: string }>;
} }
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params; const { id } = await params;
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
include: { include: {
_count: { _count: {
@ -68,17 +68,17 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
export default async function ProfilePage({ params }: Props) { export default async function ProfilePage({ params }: Props) {
const session = await auth(); const session = await auth();
const { slug } = await params; const { id } = await params;
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: Number(slug), id: Number(id),
}, },
}); });
if (!user) redirect("/404"); if (!user) redirect("/404");
const likedMiis = await prisma.like.count({ where: { userId: Number(slug) } }); const likedMiis = await prisma.like.count({ where: { userId: Number(id) } });
return ( return (
<div> <div>
@ -102,7 +102,7 @@ export default async function ProfilePage({ params }: Props) {
Created: {user?.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })} Created: {user?.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
</h4> </h4>
{session?.user.id == slug && ( {session?.user.id == id && (
<Link href="/profile/settings" className="pill button absolute right-0 bottom-0 !px-4"> <Link href="/profile/settings" className="pill button absolute right-0 bottom-0 !px-4">
<Icon icon="material-symbols:settings-rounded" className="text-2xl mr-2" /> <Icon icon="material-symbols:settings-rounded" className="text-2xl mr-2" />
<span>Settings</span> <span>Settings</span>

View file

@ -78,7 +78,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes }: Props) {
<p className="text-sm text-zinc-500">Are you sure? This will delete your Mii permanently. This action cannot be undone.</p> <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"> <div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 flex">
<Image src={`/mii/${miiId}/mii.webp`} alt="mii image" width={128} height={128} /> <Image src={`/mii/${miiId}/image?type=mii`} alt="mii image" width={128} height={128} />
<div className="p-4"> <div className="p-4">
<p className="text-xl font-bold line-clamp-1" title={miiName}> <p className="text-xl font-bold line-clamp-1" title={miiName}>
{miiName} {miiName}

View file

@ -83,9 +83,9 @@ export default function MiiList({ isLoggedIn, userId, sessionUserId }: Props) {
> >
<Carousel <Carousel
images={[ images={[
`/mii/${mii.id}/mii.webp`, `/mii/${mii.id}/image?type=mii`,
`/mii/${mii.id}/qr-code.webp`, `/mii/${mii.id}/image?type=qr-code`,
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image${index}.webp`), ...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
]} ]}
/> />

View file

@ -91,7 +91,7 @@ export default function EditForm({ mii, likes }: Props) {
try { try {
const existing = await Promise.all( const existing = await Promise.all(
Array.from({ length: mii.imageCount }, async (_, index) => { Array.from({ length: mii.imageCount }, async (_, index) => {
const path = `/mii/${mii.id}/image${index}.webp`; const path = `/mii/${mii.id}/image?type=image${index}`;
const response = await fetch(path); const response = await fetch(path);
const blob = await response.blob(); const blob = await response.blob();
@ -112,7 +112,9 @@ export default function EditForm({ mii, likes }: Props) {
<form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
<Carousel images={[`/mii/${mii.id}/mii.webp`, `/mii/${mii.id}/qr-code.webp`, ...files.map((file) => URL.createObjectURL(file))]} /> <Carousel
images={[`/mii/${mii.id}/image?type=mii`, `/mii/${mii.id}/image?type=qr-code`, ...files.map((file) => URL.createObjectURL(file))]}
/>
<div className="p-4 flex flex-col gap-1 h-full"> <div className="p-4 flex flex-col gap-1 h-full">
<h1 className="font-bold text-2xl line-clamp-1" title={name}> <h1 className="font-bold text-2xl line-clamp-1" title={name}>