アップロードを完全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:
@@ -6,9 +6,7 @@ import { API_BASE, api, type DashboardData, type Notice, type Visibility } from
|
||||
|
||||
type SourceInput = Pick<SourceRecord, "name" | "subtitle" | "description" | "visibility"> & Partial<Pick<SourceRecord, "iconURL" | "headerURL" | "website" | "tintColor">>;
|
||||
|
||||
type UploadPlan =
|
||||
| { mode: "single"; uploadUrl: string }
|
||||
| { mode: "multipart"; uploadId: string; partSizeBytes: number; parts: Array<{ partNumber: number; uploadUrl: string }> };
|
||||
type UploadPlan = { mode: "single"; uploadUrl: string };
|
||||
|
||||
type AppPatch = Partial<Pick<AppRecord, "name" | "developerName" | "subtitle" | "localizedDescription" | "iconURL" | "category" | "appPermissions">>;
|
||||
|
||||
@@ -98,22 +96,12 @@ export function DashboardProvider({ children }: PropsWithChildren) {
|
||||
setNotice({ tone: "info", text: "ADPをアップロードし、Manifestとアセットを検証しています。" });
|
||||
try {
|
||||
const init = await api<{ upload: { id: string }; uploadPlan: UploadPlan }>("/api/v1/uploads", { method: "POST", body: JSON.stringify({ sourceId, appId, filename: file.name, sizeBytes: file.size }) });
|
||||
const uploadedParts: Array<{ partNumber: number; etag: string }> = [];
|
||||
if (init.uploadPlan.mode === "multipart") {
|
||||
for (const part of init.uploadPlan.parts) {
|
||||
const start = (part.partNumber - 1) * init.uploadPlan.partSizeBytes;
|
||||
const response = await fetch(part.uploadUrl, { method: "PUT", body: file.slice(start, Math.min(file.size, start + init.uploadPlan.partSizeBytes)) });
|
||||
if (!response.ok) throw new Error(`ADPパート${part.partNumber}の転送に失敗しました。`);
|
||||
const etag = response.headers.get("etag");
|
||||
if (!etag) throw new Error(`ADPパート${part.partNumber}のETagを取得できませんでした。`);
|
||||
uploadedParts.push({ partNumber: part.partNumber, etag });
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(init.uploadPlan.uploadUrl, { method: "PUT", headers: { "content-type": "application/zip" }, body: file });
|
||||
if (!response.ok) throw new Error("ADPファイルの転送に失敗しました。");
|
||||
}
|
||||
// Uploads always go through the API, which streams to MinIO internally,
|
||||
// so a single PUT is all that's needed (no presigned URLs, no multipart).
|
||||
const response = await fetch(init.uploadPlan.uploadUrl, { method: "PUT", headers: { "content-type": "application/zip" }, body: file });
|
||||
if (!response.ok) throw new Error("ADPファイルの転送に失敗しました。");
|
||||
|
||||
let complete = await api<{ release?: ReleaseRecord }>(`/api/v1/uploads/${init.upload.id}/complete`, { method: "POST", body: JSON.stringify({ parts: uploadedParts.length ? uploadedParts : undefined }) });
|
||||
let complete = await api<{ release?: ReleaseRecord }>(`/api/v1/uploads/${init.upload.id}/complete`, { method: "POST", body: "{}" });
|
||||
if (!complete.release) {
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
|
||||
Reference in New Issue
Block a user