50 lines
2.4 KiB
JavaScript
50 lines
2.4 KiB
JavaScript
import { digest } from './releaseArtifacts.mjs'
|
|
import { validateFeedUrl } from '../build/config.mjs'
|
|
import { randomBytes } from 'node:crypto'
|
|
|
|
export const UPLOAD_CHUNK_SIZE = 512 * 1024
|
|
|
|
/** Publish immutable assets first and the feed pointer last, verifying public bytes. */
|
|
export async function uploadRelease(release, { uploadUrl, token, fetchImpl = fetch, log = console.log }) {
|
|
uploadUrl = validateFeedUrl(uploadUrl)
|
|
if (!token || /[\r\n]/.test(token)) throw new Error('Set CAPSULE_UPLOAD_TOKEN in the release environment.')
|
|
for (const file of release.files) {
|
|
const metadata = file.name === 'latest.yml'
|
|
const uploadId = randomBytes(16).toString('hex')
|
|
const chunked = !metadata && file.data.length > UPLOAD_CHUNK_SIZE
|
|
const step = chunked ? UPLOAD_CHUNK_SIZE : file.data.length
|
|
if (!step) throw new Error(`Cannot upload empty artifact ${file.name}`)
|
|
for (let offset = 0; offset < file.data.length; offset += step) {
|
|
const body = file.data.subarray(offset, offset + step)
|
|
const response = await fetchImpl(new URL(file.name, uploadUrl), {
|
|
method: 'PUT',
|
|
redirect: 'error',
|
|
signal: AbortSignal.timeout(10 * 60 * 1000),
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': metadata ? 'application/yaml' : 'application/octet-stream',
|
|
'Cache-Control': metadata ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
...(!metadata ? { 'If-None-Match': '*' } : {}),
|
|
...(chunked ? {
|
|
'Content-Range': `bytes ${offset}-${offset + body.length - 1}/${file.data.length}`,
|
|
'X-Capsule-Upload-Id': uploadId
|
|
} : {})
|
|
},
|
|
body
|
|
})
|
|
// Existing immutable files are safe to reuse only after verifying their public bytes.
|
|
if (!response.ok && !(response.status === 412 && !metadata)) {
|
|
throw new Error(`Upload failed for ${file.name}: HTTP ${response.status}. Publication stopped.`)
|
|
}
|
|
if (response.status === 412) break
|
|
}
|
|
const publicResponse = await fetchImpl(new URL(file.name, release.feedUrl), {
|
|
redirect: 'error', signal: AbortSignal.timeout(10 * 60 * 1000), cache: 'no-store'
|
|
})
|
|
if (!publicResponse.ok || digest(Buffer.from(await publicResponse.arrayBuffer())) !== digest(file.data)) {
|
|
throw new Error(`Public verification failed for ${file.name}. Publication stopped.`)
|
|
}
|
|
log(`Published and verified ${file.name}`)
|
|
}
|
|
}
|