Files
toolkit/packages/artifact/src/internal/utils.ts
T

184 lines
5.4 KiB
TypeScript
Raw Normal View History

2020-02-11 09:49:46 -05:00
import {debug} from '@actions/core'
2020-02-13 18:24:11 -05:00
import {promises as fs} from 'fs'
2020-02-11 09:49:46 -05:00
import {HttpCodes, HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth'
import {IHeaders} from '@actions/http-client/interfaces'
import {
getRuntimeToken,
getRuntimeUrl,
getWorkFlowRunId
2020-03-12 14:50:27 +01:00
} from './config-variables'
2020-02-11 09:49:46 -05:00
/**
* Parses a env variable that is a number
*/
export function parseEnvNumber(key: string): number | undefined {
const value = Number(process.env[key])
if (Number.isNaN(value) || value < 0) {
return undefined
}
return value
}
/**
* Various utility functions to help with the necessary API calls
*/
export function getApiVersion(): string {
return '6.0-preview'
}
export function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return false
}
return statusCode >= 200 && statusCode < 300
}
export function isRetryableStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return false
}
const retryableStatusCodes = [
HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout
]
return retryableStatusCodes.includes(statusCode)
}
export function getContentRange(
start: number,
end: number,
total: number
): string {
// Format: `bytes start-end/fileSize
// start and end are inclusive
// For a 200 byte chunk starting at byte 0:
// Content-Range: bytes 0-199/200
return `bytes ${start}-${end}/${total}`
}
2020-03-12 14:50:27 +01:00
/**
* Sets all the necessary headers when making HTTP calls
* @param {string} contentType the type of content being uploaded
* @param {boolean} isKeepAlive is the same connection being used to make multiple calls
* @param {boolean} isGzip is the connection being used to upload GZip compressed content
* @param {number} uncompressedLength the original size of the content if something is being uploaded that has been compressed
* @param {number} contentLength the length of the content that is being uploaded
* @param {string} contentRange the range of the content that is being uploaded
* @returns appropriate request options to make a specific http call
*/
2020-02-11 09:49:46 -05:00
export function getRequestOptions(
contentType?: string,
2020-03-12 14:50:27 +01:00
isKeepAlive?: boolean,
isGzip?: boolean,
uncompressedLength?: number,
2020-02-11 09:49:46 -05:00
contentLength?: number,
contentRange?: string
): IHeaders {
const requestOptions: IHeaders = {
2020-03-12 14:50:27 +01:00
// same Accept type for each http call that gets made
2020-02-11 09:49:46 -05:00
Accept: `application/json;api-version=${getApiVersion()}`
}
if (contentType) {
requestOptions['Content-Type'] = contentType
}
2020-03-12 14:50:27 +01:00
if (isKeepAlive) {
requestOptions['Connection'] = 'Keep-Alive'
// keep alive for at least 10 seconds before closing the connection
requestOptions['Keep-Alive'] = '10'
}
if (isGzip) {
requestOptions['Content-Encoding'] = 'gzip'
requestOptions['x-tfs-filelength'] = uncompressedLength
}
2020-02-11 09:49:46 -05:00
if (contentLength) {
requestOptions['Content-Length'] = contentLength
}
if (contentRange) {
requestOptions['Content-Range'] = contentRange
}
return requestOptions
}
export function createHttpClient(): HttpClient {
return new HttpClient('action/artifact', [
new BearerCredentialHandler(getRuntimeToken())
])
}
export function getArtifactUrl(): string {
const artifactUrl = `${getRuntimeUrl()}_apis/pipelines/workflows/${getWorkFlowRunId()}/artifacts?api-version=${getApiVersion()}`
debug(`Artifact Url: ${artifactUrl}`)
return artifactUrl
}
/**
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
* individual filesystem/platform will not be supported on all fileSystems/platforms
2020-03-12 14:50:27 +01:00
*
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
2020-02-11 09:49:46 -05:00
*/
2020-03-12 14:50:27 +01:00
const invalidArtifactFilePathCharacters = [
'"',
':',
'<',
'>',
'|',
'*',
'?',
' '
]
const invalidArtifactNameCharacters = [
...invalidArtifactFilePathCharacters,
'\\',
'/'
]
2020-02-11 09:49:46 -05:00
/**
2020-03-12 14:50:27 +01:00
* Scans the name of the artifact to make sure there are no illegal characters
2020-02-11 09:49:46 -05:00
*/
export function checkArtifactName(name: string): void {
if (!name) {
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
}
2020-03-12 14:50:27 +01:00
for (const invalidChar of invalidArtifactNameCharacters) {
2020-02-11 09:49:46 -05:00
if (name.includes(invalidChar)) {
throw new Error(
2020-03-12 14:50:27 +01:00
`Artifact name is not valid: ${name}. Contains character: "${invalidChar}". Invalid artifact name characters include: ${invalidArtifactNameCharacters.toString()}.`
)
}
}
}
/**
* Scans the name of the filePath used to make sure there are no illegal characters
*/
export function checkArtifactFilePath(path: string): void {
if (!path) {
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
}
for (const invalidChar of invalidArtifactFilePathCharacters) {
if (path.includes(invalidChar)) {
throw new Error(
`Artifact path is not valid: ${path}. Contains character: "${invalidChar}". Invalid characters include: ${invalidArtifactFilePathCharacters.toString()}.`
2020-02-11 09:49:46 -05:00
)
}
}
}
2020-02-13 18:24:11 -05:00
export async function createDirectoriesForArtifact(
directories: string[]
): Promise<void> {
for (const directory of directories) {
await fs.mkdir(directory, {
recursive: true
})
}
}