- Fastify API・Node Worker・PostgreSQL・S3互換ストレージ構成
- ADP ZIPのマルチパートアップロードとManifest検証(パストラバーサル・ZIP爆弾等を拒否)
- manifest.json/signatureは再シリアライズせず元バイト列を保持
- Cloudflare風の運用向け管理ダッシュボード(shadcn/Radix・日本語UI・4ルート)
概要/Sources/アプリ/リリース + ルートランディングページ
- 汎用OIDC SSO・Workspace単位の認可・demo mode
- 匿名配布: /sources/{slug}/source.json, /artifacts/{releaseId}/manifest.json
- Docker Compose対応
67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import type { AppMetadata } from "../../../packages/core/src/index";
|
|
import { inspectAdpArchive, storeAdpAssets } from "./manifest";
|
|
import type { AppConfig } from "./config";
|
|
import type { Store } from "./store";
|
|
import type { StorageAdapter } from "./storage";
|
|
|
|
export async function processUpload(uploadId: string, store: Store, storage: StorageAdapter, appConfig: AppConfig) {
|
|
const upload = await store.getUpload(uploadId);
|
|
if (!upload) throw new Error("UPLOAD_NOT_FOUND");
|
|
await store.updateUpload(uploadId, { status: "processing", errorCode: undefined, errorMessage: undefined });
|
|
const workDir = await mkdtemp(join(tmpdir(), "altdock-upload-"));
|
|
const archivePath = join(workDir, "package.zip");
|
|
try {
|
|
await storage.downloadToFile(upload.objectKey, archivePath);
|
|
const inspection = await inspectAdpArchive(archivePath, appConfig.MAX_ARCHIVE_ENTRIES, appConfig.MAX_EXPANDED_BYTES);
|
|
const source = await store.getSource(upload.sourceId);
|
|
if (!source) throw new Error("SOURCE_NOT_FOUND");
|
|
const existingApp = upload.appId ? await store.getApp(upload.appId) : null;
|
|
const metadata: Partial<AppMetadata> = existingApp || {
|
|
name: source.name,
|
|
developerName: "AltDock Developer",
|
|
localizedDescription: `Release ${inspection.summary.shortVersionString} (${inspection.summary.bundleVersion})`,
|
|
subtitle: "",
|
|
tintColor: source.tintColor,
|
|
category: "other",
|
|
screenshots: [],
|
|
appPermissions: { entitlements: [], privacy: {} },
|
|
};
|
|
const app = await store.upsertApp(upload.sourceId, {
|
|
bundleIdentifier: inspection.summary.bundleId,
|
|
marketplaceID: inspection.summary.appleItemId,
|
|
metadata,
|
|
});
|
|
await store.updateUpload(uploadId, { appId: app.id });
|
|
const releaseId = randomUUID();
|
|
const release = await store.createRelease({
|
|
id: releaseId,
|
|
appId: app.id,
|
|
uploadId,
|
|
version: inspection.summary.shortVersionString,
|
|
buildVersion: inspection.summary.bundleVersion,
|
|
appleItemId: inspection.summary.appleItemId,
|
|
date: new Date().toISOString(),
|
|
localizedDescription: metadata.localizedDescription || "",
|
|
minOSVersion: inspection.summary.minimumSystemVersions.ios,
|
|
sizeBytes: 0,
|
|
manifest: inspection.summary,
|
|
});
|
|
const result = await storeAdpAssets(archivePath, release.id, inspection, storage);
|
|
await store.addReleaseAssets(result.assets);
|
|
const completed = await store.updateRelease(release.id, { sizeBytes: result.totalBytes, status: "ready" });
|
|
await store.updateUpload(uploadId, { status: "completed", receivedSize: upload.receivedSize || upload.expectedSize });
|
|
return completed;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "ADP_PROCESSING_FAILED";
|
|
console.error(`[altdock] upload ${uploadId} failed: ${message}`);
|
|
await store.updateUpload(uploadId, { status: "failed", errorCode: message.split(":")[0], errorMessage: message });
|
|
throw error;
|
|
} finally {
|
|
await rm(workDir, { recursive: true, force: true });
|
|
}
|
|
}
|