blob: 3f0f97691b74dc063dadb7d30f471993321cb8e9 [file] [log] [blame]
const DownloadHttpClient = require('@actions/artifact/lib/internal/download-http-client.js')
const utils = require('@actions/artifact/lib/internal/utils.js')
const config_variables = require('@actions/artifact/lib/internal/config-variables.js')
const download_specification = require('@actions/artifact/lib/internal/download-specification.js')
class ExtendedDownloadHttpClient extends DownloadHttpClient.DownloadHttpClient {
constructor() {
super()
}
async downloadStream(name, outputStream) {
const artifacts = await this.listArtifacts()
if (artifacts.count === 0) {
throw new Error(
`Unable to find any artifacts for the associated workflow`
)
}
const artifactToDownload = artifacts.value.find(artifact => {
return artifact.name === name
})
if (!artifactToDownload) {
throw new Error(`Unable to find an artifact with the name: ${name}`)
}
const partNameRegex = new RegExp(`${name}/part\\d+`)
const items = await this.getContainerItems(
artifactToDownload.name,
artifactToDownload.fileContainerResourceUrl
)
const entries = items.value
.filter(entry => {
return entry.path.match(partNameRegex)
})
.sort((a, b) => a.path.localeCompare(b.path))
const downloadSpecification =
download_specification.getDownloadSpecification(name, entries, '', false)
if (downloadSpecification.filesToDownload.length === 0) {
console.error(
`No downloadable files were found for the artifact: ${artifactToDownload.name}`
)
} else {
await this.downloadSingleArtifactToStream(
downloadSpecification.filesToDownload,
outputStream
)
}
}
async downloadSingleArtifactToStream(downloadItems, outputStream) {
let currentFile = 0
console.error(
`Total number of files that will be downloaded: ${downloadItems.length}`
)
this.statusReporter.setTotalNumberOfFilesToProcess(downloadItems.length)
this.statusReporter.start()
try {
while (currentFile < downloadItems.length) {
const currentFileToDownload = downloadItems[currentFile]
currentFile += 1
await this.downloadIndividualFileToStream(
0,
currentFileToDownload.sourceLocation,
outputStream
)
this.statusReporter.incrementProcessedCount()
}
} catch (error) {
throw new Error(`Unable to download the artifact: ${error}`)
} finally {
this.statusReporter.stop()
// safety dispose all connections
this.downloadHttpManager.disposeAndReplaceAllClients()
}
}
async downloadIndividualFileToStream(
httpClientIndex,
artifactLocation,
outputStream
) {
let retryCount = 0
const retryLimit = config_variables.getRetryLimit()
const headers = utils.getDownloadHeaders('application/json', true, true)
// a single GET request is used to download a file
const makeDownloadRequest = async () => {
const client = this.downloadHttpManager.getClient(httpClientIndex)
return await client.get(artifactLocation, headers)
}
// Increments the current retry count and then checks if the retry limit has been reached
// If there have been too many retries, fail so the download stops. If there is a retryAfterValue value provided,
// it will be used
const backOff = async retryAfterValue => {
retryCount++
if (retryCount > retryLimit) {
throw new Error(
`Retry limit has been reached. Unable to download ${artifactLocation}`
)
} else {
this.downloadHttpManager.disposeAndReplaceClient(httpClientIndex)
if (retryAfterValue) {
// Back off by waiting the specified time denoted by the retry-after header
console.error(
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the download`
)
await utils.sleep(retryAfterValue)
} else {
// Back off using an exponential value that depends on the retry count
const backoffTime =
utils.getExponentialRetryTimeInMilliseconds(retryCount)
console.error(
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the download`
)
await utils.sleep(backoffTime)
}
console.error(
`Finished backoff for retry #${retryCount}, continuing with download`
)
}
}
var partBuffer
let partIndex = 0
// keep trying to download a file until a retry limit has been reached
while (retryCount <= retryLimit) {
let response
try {
response = await makeDownloadRequest()
} catch (error) {
// if an error is caught, it is usually indicative of a timeout so retry the download
console.error(
'An error occurred while attempting to download a file',
error
)
// increment the retryCount and use exponential backoff to wait before making the next request
await backOff()
continue
}
let forceRetry = false
if (utils.isSuccessStatusCode(response.message.statusCode)) {
try {
const partSize = parseInt(response.message.headers['content-length'])
if (!partBuffer || partBuffer.length != partSize) {
partBuffer = Buffer.alloc(partSize)
}
partIndex = 0
await new Promise((resolve, reject) => {
response.message
.on('error', error => {
console.error(
`An error occurred while attempting to read the response stream`,
error
)
reject(error)
})
.on('data', chunk => {
chunk.copy(partBuffer, partIndex)
partIndex += chunk.length
})
.on('close', () => {
resolve()
})
})
if (partSize != partIndex) {
console.error(
`Didn't receive full file. received bytes ${partIndex} != expected bytes ${partSize}`
)
}
break
} catch (error) {
forceRetry = true
}
}
if (
forceRetry ||
utils.isRetryableStatusCode(response.message.statusCode)
) {
console.error(
`A ${response.message.statusCode} response code has been received while attempting to download an artifact`
)
// if a throttled status code is received, try to get the retryAfter header value, else differ to standard exponential backoff
utils.isThrottledStatusCode(response.message.statusCode)
? await backOff(
utils.tryGetRetryAfterValueTimeInMilliseconds(
response.message.headers
)
)
: await backOff()
} else {
// Some unexpected response code, fail immediately and stop the download
utils.displayHttpDiagnostics(response)
throw new Error(
`Unexpected http ${response.message.statusCode} during download for ${artifactLocation}`
)
}
}
outputStream.write(partBuffer)
}
}
module.exports = ExtendedDownloadHttpClient