アップロードを完全API経由に変更し、MinIOを非公開化
- presigned URL / multipartアップロードを廃止し、ブラウザ→API→MinIOの中継に統一 - S3Storage.createUploadPlan は常にsingle(API内部エンドポイント)を返す - PUT /api/v1/uploads/content/:encodedKey をS3モードでも有効化しガード削除 - body parserをstream化し5GiBをメモリに載せないよう修正 - フロントのmultipart分岐を削除しsingle PUTに一本化 これでMinIOに公開ドメインが不要になり、Coolifyではapi/webのドメイン設定だけで完結する
This commit is contained in:
+7
-13
@@ -7,15 +7,12 @@ import { Readable } from "node:stream";
|
||||
import {
|
||||
CreateBucketCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommand,
|
||||
HeadObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
UploadPartCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import type { AppConfig } from "./config";
|
||||
|
||||
export interface StoredObject {
|
||||
@@ -25,8 +22,7 @@ export interface StoredObject {
|
||||
}
|
||||
|
||||
export type UploadPlan =
|
||||
| { mode: "single"; uploadUrl: string }
|
||||
| { mode: "multipart"; uploadId: string; partSizeBytes: number; parts: Array<{ partNumber: number; uploadUrl: string }> };
|
||||
{ mode: "single"; uploadUrl: string };
|
||||
|
||||
export interface StorageAdapter {
|
||||
createUploadPlan(key: string, contentType: string, sizeBytes: number): Promise<UploadPlan>;
|
||||
@@ -149,14 +145,12 @@ export class S3Storage implements StorageAdapter {
|
||||
}
|
||||
|
||||
|
||||
async createUploadPlan(key: string, contentType: string, sizeBytes: number) {
|
||||
const partSizeBytes = 16 * 1024 * 1024;
|
||||
const partCount = Math.ceil(sizeBytes / partSizeBytes);
|
||||
if (partCount > 10000) throw new Error("UPLOAD_PART_COUNT_LIMIT");
|
||||
const multipart = await this.client.send(new CreateMultipartUploadCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, ContentType: contentType }));
|
||||
if (!multipart.UploadId) throw new Error("MULTIPART_UPLOAD_ID_MISSING");
|
||||
const parts = await Promise.all(Array.from({ length: partCount }, async (_, index) => ({ partNumber: index + 1, uploadUrl: await getSignedUrl(this.client, new UploadPartCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, UploadId: multipart.UploadId, PartNumber: index + 1 }), { expiresIn: 900 }) })));
|
||||
return { mode: "multipart" as const, uploadId: multipart.UploadId, partSizeBytes, parts };
|
||||
// Uploads always flow through the API (browser -> API -> MinIO), so there is
|
||||
// no presigned/multipart plan returned to the client. The API exposes a
|
||||
// single PUT endpoint that streams the body straight into object storage,
|
||||
// which keeps MinIO fully private and removes the need for a public S3 URL.
|
||||
async createUploadPlan(key: string) {
|
||||
return { mode: "single" as const, uploadUrl: `/api/v1/uploads/content/${encodeURIComponent(key)}` };
|
||||
}
|
||||
|
||||
async completeMultipartUpload(key: string, uploadId: string | undefined, parts: Array<{ partNumber: number; etag: string }> | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user