diff --git a/packages/artifact/src/internal/upload-http-client.ts b/packages/artifact/src/internal/upload-http-client.ts index 9892a42d..a9a77f32 100644 --- a/packages/artifact/src/internal/upload-http-client.ts +++ b/packages/artifact/src/internal/upload-http-client.ts @@ -9,6 +9,7 @@ import { UploadResults } from './contracts' import { + digestForStream, getArtifactUrl, getContentRange, getUploadHeaders, @@ -406,6 +407,9 @@ export class UploadHttpClient { isGzip: boolean, totalFileSize: number ): Promise { + // open a new stream and read it to compute the digest + const digest = await digestForStream(openStream()) + // prepare all the necessary headers before making any http call const headers = getUploadHeaders( 'application/octet-stream', @@ -413,7 +417,8 @@ export class UploadHttpClient { isGzip, totalFileSize, end - start + 1, - getContentRange(start, end, uploadFileSize) + getContentRange(start, end, uploadFileSize), + digest ) const uploadChunkRequest = async (): Promise => { diff --git a/packages/artifact/src/internal/utils.ts b/packages/artifact/src/internal/utils.ts index f2b99f33..e23056f6 100644 --- a/packages/artifact/src/internal/utils.ts +++ b/packages/artifact/src/internal/utils.ts @@ -1,9 +1,10 @@ -import {debug, info, warning} from '@actions/core' +import * as crypto from 'crypto' import {promises as fs} from 'fs' +import {IncomingHttpHeaders} from 'http' +import {debug, info, warning} from '@actions/core' import {HttpCodes, HttpClient} from '@actions/http-client' import {BearerCredentialHandler} from '@actions/http-client/auth' import {IHeaders, IHttpClientResponse} from '@actions/http-client/interfaces' -import {IncomingHttpHeaders} from 'http' import { getRuntimeToken, getRuntimeUrl, @@ -180,7 +181,8 @@ export function getUploadHeaders( isGzip?: boolean, uncompressedLength?: number, contentLength?: number, - contentRange?: string + contentRange?: string, + digest?: string ): IHeaders { const requestOptions: IHeaders = {} requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}` @@ -202,6 +204,10 @@ export function getUploadHeaders( if (contentRange) { requestOptions['Content-Range'] = contentRange } + if (digest) { + // TODO(robherley): should we use 'Digest' directly? https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Digest + requestOptions['X-Digest'] = `sha-256=${digest}` + } return requestOptions } @@ -291,3 +297,15 @@ export function getProperRetention( export async function sleep(milliseconds: number): Promise { return new Promise(resolve => setTimeout(resolve, milliseconds)) } + +export async function digestForStream( + stream: NodeJS.ReadableStream +): Promise { + return new Promise((resolve, reject) => { + // TODO(robherley): switch to crc64 for production + const hasher = crypto.createHash('sha256') + stream.on('data', data => hasher.update(data)) + stream.on('end', () => resolve(hasher.digest('hex'))) + stream.on('error', reject) + }) +}