d3ro-voice/scripts/ci/lib/forgejo-generic-registry.mjs

76 lines
3.2 KiB
JavaScript

// scripts/ci/lib/forgejo-generic-registry.mjs
// Forgejo generic package registry IO 어댑터 (portable-publish-policy.mjs 의 PackageRegistry 포트).
//
// - Forgejo generic registry는 HEAD를 405로 거부하고 파일 해시를 헤더로 주지 않는다.
// 패키지 버전의 파일 목록 API는 sha256을 주므로, 큰 볼륨을 내려받지 않고도 원격 바이트가
// 로컬과 같은지 정확히 판단할 수 있다. (Range GET의 크기만 비교하면 90MiB 볼륨에서 크기가
// 우연히 같을 때 다른 바이트를 "동일"로 오판한다 — 실측 사고.)
// - 404 외의 오류는 모두 예외로 올린다(fail-closed). 원격 상태를 모르면 게시 판단을 할 수 없다.
/**
* @param {{
* feedUrl: string,
* packageApiUrl: string,
* fetchImpl: (url: string, init?: RequestInit) => Promise<Response>,
* onUploaded?: (url: string) => void,
* }} deps
* @returns {import("./portable-publish-policy.mjs").PackageRegistry & { fileUrl: (versionPath: string, name: string) => string }}
*/
export function createForgejoGenericRegistry({ feedUrl, packageApiUrl, fetchImpl, onUploaded }) {
const versionApi = (versionPath) => `${packageApiUrl}/${encodeURIComponent(versionPath)}`;
const fileUrl = (versionPath, name) =>
`${feedUrl}/${encodeURIComponent(versionPath)}/${encodeURIComponent(name)}`;
return {
fileUrl,
async listFileHashes(versionPath) {
const response = await fetchImpl(`${versionApi(versionPath)}/files`);
if (response.status === 404) return new Map();
if (!response.ok) {
throw new Error(`파일 목록 조회 실패 (HTTP ${response.status}): ${versionPath}`);
}
const files = await response.json();
if (!Array.isArray(files)) {
throw new Error(`파일 목록 응답 형식이 올바르지 않습니다: ${versionPath}`);
}
return new Map(files.map((file) => [String(file.name), String(file.sha256)]));
},
async deleteVersion(versionPath) {
const response = await fetchImpl(versionApi(versionPath), { method: "DELETE" });
if (!response.ok && response.status !== 404) {
throw new Error(`버전 삭제 실패 (HTTP ${response.status}): ${versionPath}`);
}
},
async uploadFile(versionPath, file) {
const url = fileUrl(versionPath, file.name);
const response = await fetchImpl(url, {
method: "PUT",
headers: { "Content-Type": file.contentType },
body: file.bytes,
});
if (!response.ok) {
throw new Error(
`업로드 실패 (HTTP ${response.status}): ${url}\n` +
" HTTP 413이면 Cloudflare 본문 한도(100MiB) 초과입니다. 볼륨 크기를 줄이세요.",
);
}
onUploaded?.(url);
},
async readTextFile(versionPath, name) {
const response = await fetchImpl(`${fileUrl(versionPath, name)}?ts=${Date.now()}`, {
cache: "no-store",
});
if (response.status === 404) return undefined;
if (!response.ok) {
throw new Error(
`게시된 파일을 읽을 수 없습니다 (HTTP ${response.status}): ${versionPath}/${name}`,
);
}
return response.text();
},
};
}