آپلود تصویر به پارس‌پک

parspack-s3-uploadراهنما

آپلود سرورساید تصویر به فضای ابری پارس‌پک با AWS SDK، path-style URL، اعتبارسنجی MIME و مسیر API در Next.js App Router. آماده برای تحویل به مدل یا توسعه‌دهنده.

کی به کار می‌آد

  • آپلود تصویر پروفایل، محصول یا گالری
  • اتصال به S3 سازگار با پارس‌پک
  • وقتی URL عمومی اشتباه ساخته میشه
  • تنظیم next/image برای هاست پارس‌پک

برچسب‌ها

راهنماS3پارس‌پکآپلود

راهنما فرانت‌متر نداره و خودکار فعال نمیشه؛ از 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-presigner

2. 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
VariableRequiredNotes
S3_ENDPOINTYesParsPack endpoint URL (no trailing slash)
S3_BUCKETYesBucket name
S3_ACCESS_KEY_IDYesParsPack access key
S3_SECRET_ACCESS_KEYYesParsPack secret key
S3_REGIONNoDefaults to us-east-1 (ParsPack often ignores region)
S3_FORCE_PATH_STYLENoDefaults 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 endpoint

4. 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>.jpg

6. 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 key if 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:

MethodPathAuthBodySuccess response
POST/api/uploadSession cookieFormData 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" (matches formData.get("file") on the server).

  • Do not set Content-Type manually on fetch - 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:

  1. forcePathStyle: true on S3Client.

  2. Public URL format: {S3_ENDPOINT}/{S3_BUCKET}/{key} - not https://bucket.endpoint/key.

  3. Bucket ACL / policy: Objects must be publicly readable if you store direct URLs.

  4. Endpoint host: Use the exact endpoint from ParsPack (e.g. https://YOUR_ACCOUNT.parspack.net).

  5. Region: us-east-1 is fine as a placeholder; ParsPack is endpoint-driven.

  6. CORS: Not needed for server-side upload. Only needed if you switch to direct browser → S3 uploads.


12. Common failure modes

SymptomLikely cause
Missing required environment variableEnv vars not set in .env.local / deployment
403 / AccessDenied from S3Wrong credentials or bucket permissions
Upload succeeds but image 403 in browserBucket/object not public
Wrong URL shapeforcePathStyle is false or URL builder uses virtual-host style
next/image brokenParsPack hostname missing from remotePatterns
401 on uploadNo session cookie; user not logged in
File too large / Invalid file typeServer validation - check MIME and 5 MB limit

13. Optional extensions

  • Delete on record removal: DeleteObjectCommand using stored key.

  • Private bucket: Skip getPublicObjectUrl, use getPresignedObjectUrl when serving.

  • Direct browser upload: Presigned PutObject URL to reduce server memory use for large files.

  • Image processing: Resize/compress with sharp before PutObject.


14. Minimal test plan

  1. Set all S3_* env vars from ParsPack.

  2. Log in as an authenticated user.

  3. POST /api/upload with a small JPEG via curl or the UI.

  4. Confirm response: { "url": "https://...parspack.net/bucket/uploads/...", "key": "uploads/..." }.

  5. Open url in a browser - image should load.

  6. Save a record with that imageUrl and 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

نمونه برای نشان دادن جهت تغییره؛ خروجی واقعی به مدل و پرامپت شما بستگی داره.

نصب

  1. با CLI

    این دستور فایل را در docs/parspack-s3-upload.md می‌نویسه. CLI به init نیاز نداره؛ فقط باید داخل پوشه‌ی پروژه باشید.

    $npx vibefarsi add parspack-s3-upload
  2. دستی

    محتوای تب 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.md خلاصه‌ی همه‌ی مهارت‌ها در یک صفحه‌ست و قوانین کرافت رابط طرف طراحی را پوشش میده. npx vibefarsi init هر دو را داخل پروژه می‌نویسه.