@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
153 lines • 5.86 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { ux } from '@oclif/core';
import got from 'got';
import { SfError } from '@salesforce/core';
import chalk from 'chalk';
import { S3 } from '@aws-sdk/client-s3';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { isString } from '@salesforce/ts-types';
import { api } from './codeSigning/packAndSign.js';
const BASE_URL = 'https://developer.salesforce.com';
const BUCKET = 'dfc-data-production';
export class AmazonS3 {
options;
static STATUS_URL = 'https://s3.amazonaws.com';
directory;
s3;
baseKey;
constructor(options) {
this.options = options;
this.directory = `https://developer.salesforce.com/media/salesforce-cli/${this.options.cli ?? ''}`;
this.baseKey = this.directory.replace(BASE_URL, '').replace(/^\//, '');
this.s3 = new S3({
region: 'us-east-1',
...resolveCredentials(options.credentials),
requestHandler: buildRequestHandler(),
});
}
// there's an abstract class for cli:install:test using this
// eslint-disable-next-line class-methods-use-this
async ping() {
const { statusCode } = await got.get(AmazonS3.STATUS_URL);
return { service: 'Amazon S3', available: statusCode >= 200 && statusCode < 300 };
}
async getManifestFromChannel(channel) {
const url = `${this.directory}/channels/${channel}/${this.options.cli}-darwin-x64-buildmanifest`;
const filename = await getFileAtUrl(url);
return JSON.parse(fs.readFileSync(filename, 'utf8'));
}
async getObject(options) {
return this.s3.getObject({
...options,
Key: options.Key?.replace(BASE_URL, '').replace(/^\//, ''),
...{ Bucket: this.options.bucket ?? BUCKET },
});
}
// Paginates listObjectV2 and returns both Contents and CommonPrefixes
async listAllObjects(key) {
const prefix = key.startsWith(this.baseKey) ? key : `${this.baseKey}/${key}/`;
const bucket = this.options.bucket ?? BUCKET;
let continuationToken;
const allContents = [];
const allCommonPrefixes = [];
// Use maximum iteration to ensure termination
const MAX_ITERATIONS = 100;
for (let i = 1; i <= MAX_ITERATIONS; i++) {
// eslint-disable-next-line no-await-in-loop
const response = await this.s3.listObjectsV2({
Bucket: bucket,
Delimiter: '/',
Prefix: prefix,
ContinuationToken: continuationToken,
});
if (response.Contents) {
allContents.push(...response.Contents);
}
if (response.CommonPrefixes) {
allCommonPrefixes.push(...response.CommonPrefixes.map((item) => item.Prefix).filter(isString));
}
if (!response.IsTruncated)
break;
if (i === MAX_ITERATIONS)
throw new SfError('Max listObjectsV2 iterations reached');
continuationToken = response.NextContinuationToken;
}
return { contents: allContents, commonPrefixes: allCommonPrefixes };
}
async listCommonPrefixes(key) {
const result = await this.listAllObjects(key);
return result.commonPrefixes;
}
async listKeyContents(key) {
const result = await this.listAllObjects(key);
return result.contents;
}
}
export const download = async (url, location, silent = false) => {
const downloadStream = got.stream(url);
const fileWriterStream = fs.createWriteStream(location);
return new Promise((resolve) => {
downloadStream.on('error', (error) => {
if (!silent)
ux.error(`Download failed: ${error.message}`);
});
fileWriterStream
.on('error', (error) => {
if (!silent)
ux.action.stop('Failed');
if (!silent)
ux.error(`Could not write file to system: ${error.message}`);
})
.on('finish', () => {
if (!silent)
ux.action.stop();
resolve();
});
if (!silent)
ux.action.start(`Downloading ${chalk.cyan(url)}`);
downloadStream.pipe(fileWriterStream);
});
};
const getFileAtUrl = async (url) => {
const availability = await fileIsAvailable(url);
if (availability.available) {
const filename = path.join(os.tmpdir(), `file${Math.random()}`);
await download(url, filename, true);
return filename;
}
else {
throw new SfError(`File at url: ${url} does not exist`);
}
};
const resolveCredentials = (credentialOptions) => {
if (credentialOptions) {
return { credentials: credentialOptions };
}
return process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
? {
credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY },
}
: {};
};
const fileIsAvailable = async (url) => {
const { statusCode } = await got.head(url, { throwHttpErrors: false });
return { service: 'file', name: url, available: statusCode >= 200 && statusCode < 300 };
};
const buildRequestHandler = () => {
const agent = api.getAgentForUri('https://s3.amazonaws.com');
const options = agent && agent.http
? {
httpAgent: agent.http,
}
: {};
return new NodeHttpHandler(options);
};
//# sourceMappingURL=amazonS3.js.map