merge: require cover for first project submission
This commit is contained in:
@@ -1020,9 +1020,22 @@ function readProjectCoverUpload(value: unknown): ProjectCoverUpload | null {
|
||||
if (dataBase64.length > Math.ceil(MAX_PROJECT_COVER_BYTES * 4 / 3) + 4) return null;
|
||||
const bytes = Buffer.from(dataBase64, 'base64');
|
||||
if (bytes.length === 0 || bytes.length > MAX_PROJECT_COVER_BYTES) return null;
|
||||
const validSignature = mimeType === 'image/png'
|
||||
? bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
||||
: mimeType === 'image/jpeg'
|
||||
? bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff
|
||||
: bytes.length >= 12 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP';
|
||||
if (!validSignature) return null;
|
||||
return { fileName, mimeType, bytes };
|
||||
}
|
||||
|
||||
function createProjectWithCoverForm(metadata: Record<string, unknown>, cover: ProjectCoverUpload): FormData {
|
||||
const form = new FormData();
|
||||
form.set('metadata', JSON.stringify(metadata));
|
||||
form.set('cover', new Blob([new Uint8Array(cover.bytes)], { type: cover.mimeType }), cover.fileName);
|
||||
return form;
|
||||
}
|
||||
|
||||
async function handlePublishProjectSource(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -1125,6 +1138,15 @@ async function handlePublishProjectSource(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!projectCover) {
|
||||
sendPublishSourceFailure(
|
||||
res,
|
||||
400,
|
||||
'PROJECT_COVER_REQUIRED',
|
||||
'首次提交必须选择有效的 PNG、JPEG 或 WebP 项目封面。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await sendPublishSourceUpstreamError(
|
||||
res,
|
||||
@@ -1135,25 +1157,12 @@ async function handlePublishProjectSource(
|
||||
return;
|
||||
}
|
||||
|
||||
if (projectCover && !existingMetadataPreserved) {
|
||||
// The cover contract has no delete or atomic project attachment, so uploading
|
||||
// before a conflicting create could leave an unreferenced private object.
|
||||
sendPublishSourceFailure(
|
||||
res,
|
||||
503,
|
||||
'WORKS_SQUARE_UNAVAILABLE',
|
||||
'发布服务暂时无法安全保存封面,请稍后重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const createResponse = existingMetadataPreserved ? null : await proxyAwareFetch(createWorksUrl('/api/projects').toString(), {
|
||||
const createResponse = existingMetadataPreserved ? null : await proxyAwareFetch(createWorksUrl('/api/projects/with-cover').toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(projectMetadata),
|
||||
body: createProjectWithCoverForm(projectMetadata, projectCover!),
|
||||
});
|
||||
if (createResponse && !createResponse.ok && createResponse.status !== 409) {
|
||||
await sendPublishSourceUpstreamError(
|
||||
@@ -1167,32 +1176,6 @@ async function handlePublishProjectSource(
|
||||
await createResponse?.body?.cancel().catch(() => undefined);
|
||||
|
||||
if (createResponse?.status === 409) {
|
||||
const ownershipResponse = await proxyAwareFetch(
|
||||
createWorksUrl(`/api/projects/mine/${encodeURIComponent(appId)}/status`).toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
},
|
||||
);
|
||||
if (!ownershipResponse.ok) {
|
||||
await sendPublishSourceUpstreamError(
|
||||
res,
|
||||
ownershipResponse,
|
||||
'PROJECT_OWNERSHIP_UNCONFIRMED',
|
||||
'这个作品 ID 已被占用,请更换后重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ownershipPayload = projectSafeStatusPayload(await readResponsePayload(ownershipResponse));
|
||||
if (!ownershipPayload) {
|
||||
sendPublishSourceFailure(
|
||||
res,
|
||||
502,
|
||||
'PROJECT_OWNERSHIP_UNCONFIRMED',
|
||||
'这个作品的归属暂时无法确认,请稍后重试。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
sendPublishSourceFailure(
|
||||
res,
|
||||
409,
|
||||
|
||||
Reference in New Issue
Block a user