// 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, * 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(); }, }; }