آپلود تصویر به پارسپک
parspack-s3-uploadراهنماآپلود سرورساید تصویر به فضای ابری پارسپک با AWS SDK، path-style URL، اعتبارسنجی MIME و مسیر API در Next.js App Router. آماده برای تحویل به مدل یا توسعهدهنده.
کی به کار میآد
- آپلود تصویر پروفایل، محصول یا گالری
- اتصال به S3 سازگار با پارسپک
- وقتی URL عمومی اشتباه ساخته میشه
- تنظیم next/image برای هاست پارسپک
برچسبها
راهنما فرانتمتر نداره و خودکار فعال نمیشه؛ از CLAUDE.md یا AGENTS.md به آن لینک بدید تا مدل در هر جلسه بخوندش.
ParsPack S3 image upload (Next.js App Router)
Implementation guide for uploading images to ParsPack (S3-compatible object storage) from a Next.js App Router project. Hand this to an AI or developer to replicate the setup from scratch.
Overview
Upload images server-side through a Next.js API route. The browser sends multipart/form-data to /api/upload; the server validates the file, uploads it to ParsPack via the AWS SDK, and returns a public URL.
Flow:
1Browser (FormData) → POST /api/upload → uploadImage() → ParsPack S3 (PutObject) → { url, key }ParsPack is treated as a generic S3-compatible provider. The critical ParsPack-specific detail is path-style URLs and how public object URLs are constructed.
1. Dependencies
Install the AWS SDK v3 S3 packages (ParsPack speaks the S3 API):
1npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner2. Environment variables
Add these to .env.local (values come from the ParsPack dashboard):
1S3_ENDPOINT=https://YOUR_ACCOUNT.parspack.net2S3_BUCKET=your-bucket-name3S3_ACCESS_KEY_ID=your-access-key4S3_SECRET_ACCESS_KEY=your-secret-key5S3_REGION=us-east-16S3_FORCE_PATH_STYLE=true| Variable | Required | Notes |
|---|---|---|
S3_ENDPOINT | Yes | ParsPack endpoint URL (no trailing slash) |
S3_BUCKET | Yes | Bucket name |
S3_ACCESS_KEY_ID | Yes | ParsPack access key |
S3_SECRET_ACCESS_KEY | Yes | ParsPack secret key |
S3_REGION | No | Defaults to us-east-1 (ParsPack often ignores region) |
S3_FORCE_PATH_STYLE | No | Defaults to true. Set to "false" only if using virtual-host style |
Bucket setup: The bucket must allow public read on uploaded objects if you use direct public URLs (this guide does). Configure that in the ParsPack panel.
3. File structure
Create these files under lib/s3/ plus one API route:
1lib/s3/2 client.ts # Singleton S3Client configured for ParsPack3 urls.ts # Public URL builder + optional presigned URLs4 upload.ts # Upload logic (validation + PutObject)5app/api/upload/6 route.ts # Authenticated POST endpoint4. S3 client (lib/s3/client.ts)
ParsPack requires a custom endpoint and path-style addressing:
1import { S3Client } from "@aws-sdk/client-s3";23function requireEnv(name: string): string {4 const value = process.env[name];5 if (!value) throw new Error(`Missing required environment variable: ${name}`);6 return value;7}89let client: S3Client | null = null;1011export function getS3Client(): S3Client {12 if (client) return client;1314 client = new S3Client({15 endpoint: requireEnv("S3_ENDPOINT"),16 region: process.env.S3_REGION ?? "us-east-1",17 credentials: {18 accessKeyId: requireEnv("S3_ACCESS_KEY_ID"),19 secretAccessKey: requireEnv("S3_SECRET_ACCESS_KEY"),20 },21 // ParsPack uses path-style: https://endpoint/bucket/key22 forcePathStyle: process.env.S3_FORCE_PATH_STYLE !== "false",23 });2425 return client;26}2728export function getS3Bucket(): string {29 return requireEnv("S3_BUCKET");30}Key ParsPack detail: forcePathStyle: true is required. Without it, the SDK may use virtual-host style (bucket.endpoint/key), which ParsPack may not support.
5. URL helpers (lib/s3/urls.ts)
Public URLs for ParsPack path-style buckets:
1import { GetObjectCommand } from "@aws-sdk/client-s3";2import { getSignedUrl } from "@aws-sdk/s3-request-presigner";3import { getS3Bucket, getS3Client } from "./client";45function trimTrailingSlash(value: string): string {6 return value.replace(/\/+$/, "");7}89/** Public object URL for ParsPack path-style buckets. */10export function getPublicObjectUrl(key: string): string {11 const endpoint = trimTrailingSlash(process.env.S3_ENDPOINT ?? "");12 const bucket = getS3Bucket();13 const normalizedKey = key.replace(/^\/+/, "");14 return `${endpoint}/${bucket}/${normalizedKey}`;15}1617/** Optional - for private buckets. */18export async function getPresignedObjectUrl(19 key: string,20 expiresIn = 360021): Promise<string> {22 const command = new GetObjectCommand({23 Bucket: getS3Bucket(),24 Key: key.replace(/^\/+/, ""),25 });26 return getSignedUrl(getS3Client(), command, { expiresIn });27}Example URL shape:
1https://YOUR_ACCOUNT.parspack.net/my-bucket/uploads/<ownerId>/<uuid>.jpg6. Upload logic (lib/s3/upload.ts)
1import { PutObjectCommand } from "@aws-sdk/client-s3";2import { randomUUID } from "crypto";3import { getPublicObjectUrl } from "./urls";4import { getS3Bucket, getS3Client } from "./client";56const ALLOWED_CONTENT_TYPES = new Map([7 ["image/jpeg", "jpg"],8 ["image/png", "png"],9 ["image/webp", "webp"],10 ["image/gif", "gif"],11]);1213const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB1415export async function uploadImage(16 ownerId: string,17 file: File18): Promise<{ url: string; key: string }> {19 if (!ALLOWED_CONTENT_TYPES.has(file.type)) {20 throw new Error("Invalid file type");21 }22 if (file.size > MAX_FILE_SIZE) {23 throw new Error("File too large");24 }2526 const extension = ALLOWED_CONTENT_TYPES.get(file.type)!;27 const key = `uploads/${ownerId}/${randomUUID()}.${extension}`;28 const buffer = Buffer.from(await file.arrayBuffer());2930 await getS3Client().send(31 new PutObjectCommand({32 Bucket: getS3Bucket(),33 Key: key,34 Body: buffer,35 ContentType: file.type,36 })37 );3839 return { key, url: getPublicObjectUrl(key) };40}Design choices:
Validate MIME type and size on the server (do not rely on client-only checks).
Key pattern:
uploads/{ownerId}/{uuid}.{ext}- scoped per owner, collision-safe.Store and return the public URL in the DB, not the S3 key.
Also return
keyif you need deletion or presigned access later.
7. API route (app/api/upload/route.ts)
Authenticated endpoint. Adapt the session helpers to your auth system:
1import { NextRequest, NextResponse } from "next/server";2import { getSession } from "@/lib/session";3import { uploadImage } from "@/lib/s3/upload";45export async function POST(request: NextRequest) {6 const session = await getSession();7 if (!session?.user?.id) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }1011 const formData = await request.formData();12 const file = formData.get("file");1314 if (!file || !(file instanceof File)) {15 return NextResponse.json({ error: "No file provided" }, { status: 400 });16 }1718 try {19 const result = await uploadImage(session.user.id, file);20 return NextResponse.json(result);21 } catch (error) {22 const message = error instanceof Error ? error.message : "Upload failed";23 const status =24 message === "Invalid file type" || message === "File too large" ? 400 : 500;25 return NextResponse.json({ error: message }, { status });26 }27}Contract:
| Method | Path | Auth | Body | Success response |
|---|---|---|---|---|
POST | /api/upload | Session cookie | FormData field file | { url: string, key: string } |
Error responses: 401, 400 (validation), 500 (S3 failure).
8. Client-side usage
1const MAX_IMAGE_SIZE = 5 * 1024 * 1024;2const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];34async function handleImageUpload(file: File) {5 // Client-side validation (mirrors server - UX only)6 if (!ALLOWED_IMAGE_TYPES.includes(file.type)) return;7 if (file.size > MAX_IMAGE_SIZE) return;89 const formData = new FormData();10 formData.append("file", file);1112 const res = await fetch("/api/upload", { method: "POST", body: formData });13 if (!res.ok) {14 const data = await res.json().catch(() => null);15 throw new Error(data?.error || "Upload failed");16 }1718 const { url } = await res.json();19 // Save url into form state, then persist with your record20 setForm((f) => ({ ...f, imageUrl: url }));21}Important:
Field name must be
"file"(matchesformData.get("file")on the server).Do not set
Content-Typemanually onfetch- the browser sets the multipart boundary.Block form submit while upload is in progress.
File input:
accept="image/jpeg,image/png,image/webp,image/gif".
9. Next.js image config (next.config.ts)
If you use next/image with ParsPack URLs, allow the ParsPack hostname:
1const nextConfig = {2 images: {3 remotePatterns: [4 {5 protocol: "https",6 hostname: "YOUR_ACCOUNT.parspack.net", // your ParsPack endpoint host7 pathname: "/**",8 },9 ],10 },11};Replace the hostname with your actual ParsPack endpoint host.
10. Auth / middleware notes
Keep /api/upload reachable from the browser. Auth belongs inside the route (session cookies or tokens), not only in middleware redirects. If your middleware protects all routes, exclude /api/upload from unauthenticated redirects so the route can return JSON 401 itself.
11. ParsPack-specific checklist
When implementing or debugging:
forcePathStyle: trueonS3Client.Public URL format:
{S3_ENDPOINT}/{S3_BUCKET}/{key}- nothttps://bucket.endpoint/key.Bucket ACL / policy: Objects must be publicly readable if you store direct URLs.
Endpoint host: Use the exact endpoint from ParsPack (e.g.
https://YOUR_ACCOUNT.parspack.net).Region:
us-east-1is fine as a placeholder; ParsPack is endpoint-driven.CORS: Not needed for server-side upload. Only needed if you switch to direct browser → S3 uploads.
12. Common failure modes
| Symptom | Likely cause |
|---|---|
Missing required environment variable | Env vars not set in .env.local / deployment |
403 / AccessDenied from S3 | Wrong credentials or bucket permissions |
| Upload succeeds but image 403 in browser | Bucket/object not public |
| Wrong URL shape | forcePathStyle is false or URL builder uses virtual-host style |
next/image broken | ParsPack hostname missing from remotePatterns |
401 on upload | No session cookie; user not logged in |
File too large / Invalid file type | Server validation - check MIME and 5 MB limit |
13. Optional extensions
Delete on record removal:
DeleteObjectCommandusing storedkey.Private bucket: Skip
getPublicObjectUrl, usegetPresignedObjectUrlwhen serving.Direct browser upload: Presigned
PutObjectURL to reduce server memory use for large files.Image processing: Resize/compress with
sharpbeforePutObject.
14. Minimal test plan
Set all
S3_*env vars from ParsPack.Log in as an authenticated user.
POST /api/uploadwith a small JPEG via curl or the UI.Confirm response:
{ "url": "https://...parspack.net/bucket/uploads/...", "key": "uploads/..." }.Open
urlin a browser - image should load.Save a record with that
imageUrland confirm it renders.
Nginx note (self-hosted)
If uploads work locally but fail with HTTP 413 behind nginx, raise client_max_body_size above your app limit (e.g. 10M) and reload nginx. Default nginx body size is 1 MB and rejects before the request reaches Node.
نمونه
پرامپت: «آپلود تصویر را به پارسپک وصل کن»
بدون مهارت
آپلود مستقیم از مرورگر به bucket با virtual-host URL
با مهارت
POST /api/upload → PutObject با forcePathStyle و URL به شکل endpoint/bucket/key
نمونه برای نشان دادن جهت تغییره؛ خروجی واقعی به مدل و پرامپت شما بستگی داره.
نصب
با CLI
این دستور فایل را در
docs/parspack-s3-upload.mdمینویسه. CLI به init نیاز نداره؛ فقط باید داخل پوشهی پروژه باشید.$npx vibefarsi add parspack-s3-uploadدستی
محتوای تب parspack-s3-upload.md را کپی کنید و در مسیر ابزار خودتون بگذارید:
- Claude Codeفایل را در
docs/parspack-s3-upload.mdبگذارید و این خط را بهCLAUDE.mdاضافه کنید:@docs/parspack-s3-upload.md - Cursorدر
.cursor/rules/parspack-s3-upload.mdcباalwaysApply: trueبالای فایل - Codex و بقیهمتن را در
AGENTS.mdبگذارید یا از همانجا به فایل لینک بدید
کپی کل فایلregistry json- Claude Codeفایل را در
میخواید همهی قوانین را یکجا داشته باشید؟ قوانین فارسی برای CLAUDE.md خلاصهی همهی مهارتها در یک صفحهست و قوانین کرافت رابط طرف طراحی را پوشش میده. npx vibefarsi init هر دو را داخل پروژه مینویسه.