292 lines
7.2 KiB
TypeScript
292 lines
7.2 KiB
TypeScript
import { Credentials, S3 } from 'aws-sdk';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import klawSync from 'klaw-sync';
|
|
import klaw, { Item } from 'klaw';
|
|
import mime from 'mime-types';
|
|
import UploadStream from 's3-stream-upload';
|
|
import { isEmpty } from 'ramda';
|
|
import { createReadStream } from 'fs-extra';
|
|
import archiver from 'archiver';
|
|
import { utils } from '@serverless/core';
|
|
|
|
import {} from '../../types';
|
|
|
|
export const getClients = (
|
|
credentials: Credentials,
|
|
region: string
|
|
): {
|
|
regular: S3;
|
|
accelerated: S3;
|
|
} => {
|
|
const params = {
|
|
region,
|
|
credentials,
|
|
};
|
|
|
|
// we need two S3 clients because creating/deleting buckets
|
|
// is not available with the acceleration feature.
|
|
return {
|
|
regular: new S3(params),
|
|
accelerated: new S3({ ...params, endpoint: `s3-accelerate.amazonaws.com` }),
|
|
};
|
|
};
|
|
|
|
const bucketCreation = async (s3: S3, Bucket: S3.BucketName): Promise<any> => {
|
|
try {
|
|
await s3.headBucket({ Bucket }).promise();
|
|
} catch (e) {
|
|
if (e.code === 'NotFound' || e.code === 'NoSuchBucket') {
|
|
await utils.sleep(2000);
|
|
return bucketCreation(s3, Bucket);
|
|
}
|
|
throw new Error(e);
|
|
}
|
|
};
|
|
|
|
export const ensureBucket = async (
|
|
s3: S3,
|
|
name: string,
|
|
debug: (message: string) => void
|
|
): Promise<any> => {
|
|
try {
|
|
debug(`Checking if bucket ${name} exists.`);
|
|
await s3.headBucket({ Bucket: name }).promise();
|
|
} catch (e) {
|
|
if (e.code === 'NotFound') {
|
|
debug(`Bucket ${name} does not exist. Creating...`);
|
|
await s3.createBucket({ Bucket: name }).promise();
|
|
// there's a race condition when using acceleration
|
|
// so we need to sleep for a couple seconds. See this issue:
|
|
// https://github.com/serverless/components/issues/428
|
|
debug(`Bucket ${name} created. Confirming it's ready...`);
|
|
await bucketCreation(s3, name);
|
|
debug(`Bucket ${name} creation confirmed.`);
|
|
} else if (e.code === 'Forbidden' && e.message === null) {
|
|
throw Error(`Forbidden: Invalid credentials or this AWS S3 bucket name may already be taken`);
|
|
} else if (e.code === 'Forbidden') {
|
|
throw Error(`Bucket name "${name}" is already taken.`);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const uploadDir = async (
|
|
s3: S3,
|
|
bucketName: string,
|
|
dirPath: string,
|
|
cacheControl?: S3.CacheControl,
|
|
options?: { keyPrefix?: string }
|
|
): Promise<void> => {
|
|
const items: ReadonlyArray<klawSync.Item> = await new Promise((resolve, reject) => {
|
|
try {
|
|
resolve(klawSync(dirPath));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
const uploadItems: Promise<S3.ManagedUpload.SendData>[] = [];
|
|
items.forEach((item) => {
|
|
if (item.stats.isDirectory()) {
|
|
return;
|
|
}
|
|
|
|
let key = path.relative(dirPath, item.path);
|
|
|
|
if (options?.keyPrefix) {
|
|
key = path.posix.join(options.keyPrefix, key);
|
|
}
|
|
|
|
// convert backslashes to forward slashes on windows
|
|
if (path.sep === '\\') {
|
|
key = key.replace(/\\/g, '/');
|
|
}
|
|
|
|
const itemParams = {
|
|
Bucket: bucketName,
|
|
Key: key,
|
|
Body: fs.readFileSync(item.path),
|
|
ContentType: mime.lookup(path.basename(item.path)) || 'application/octet-stream',
|
|
CacheControl: cacheControl,
|
|
};
|
|
|
|
uploadItems.push(s3.upload(itemParams).promise());
|
|
});
|
|
|
|
await Promise.all(uploadItems);
|
|
};
|
|
|
|
export const packAndUploadDir = async ({
|
|
s3,
|
|
bucketName,
|
|
dirPath,
|
|
key,
|
|
append = [],
|
|
cacheControl,
|
|
}: {
|
|
s3: S3;
|
|
bucketName: string;
|
|
dirPath: string;
|
|
key: string;
|
|
append?: string[];
|
|
cacheControl?: S3.CacheControl;
|
|
}): Promise<void> => {
|
|
const ignore = (await utils.readFileIfExists(path.join(dirPath, '.slsignore'))) || [];
|
|
return new Promise((resolve, reject) => {
|
|
const archive = archiver('zip', {
|
|
zlib: { level: 9 },
|
|
});
|
|
|
|
if (!isEmpty(append)) {
|
|
append.forEach((file) => {
|
|
const fileStream = createReadStream(file);
|
|
archive.append(fileStream, { name: path.basename(file) });
|
|
});
|
|
}
|
|
|
|
archive.glob(
|
|
'**/*',
|
|
{
|
|
cwd: dirPath,
|
|
ignore,
|
|
},
|
|
{}
|
|
);
|
|
|
|
archive
|
|
.pipe(
|
|
UploadStream(s3, {
|
|
Bucket: bucketName,
|
|
Key: key,
|
|
CacheControl: cacheControl,
|
|
})
|
|
)
|
|
.on('error', (err: Error) => reject(err))
|
|
.on('finish', () => resolve());
|
|
|
|
archive.finalize();
|
|
});
|
|
};
|
|
|
|
export const uploadFile = async ({
|
|
s3,
|
|
bucketName,
|
|
filePath,
|
|
key,
|
|
cacheControl,
|
|
}: {
|
|
s3: S3;
|
|
bucketName: string;
|
|
filePath: string;
|
|
key: string;
|
|
cacheControl?: S3.CacheControl;
|
|
}): Promise<void> => {
|
|
return new Promise((resolve, reject) => {
|
|
fs.createReadStream(filePath)
|
|
.pipe(
|
|
UploadStream(s3, {
|
|
Bucket: bucketName,
|
|
Key: key,
|
|
ContentType: mime.lookup(filePath) || 'application/octet-stream',
|
|
CacheControl: cacheControl,
|
|
})
|
|
)
|
|
.on('error', (err: Error) => reject(err))
|
|
.on('finish', () => resolve());
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Delete Website Bucket
|
|
*/
|
|
export const clearBucket = async (s3: S3, bucketName: string): Promise<any> => {
|
|
try {
|
|
const data = await s3.listObjects({ Bucket: bucketName }).promise();
|
|
|
|
const items = data.Contents || [];
|
|
const promises = [];
|
|
|
|
for (let i = 0; i < items.length; i += 1) {
|
|
const deleteParams = { Bucket: bucketName, Key: items[i].Key as string };
|
|
const delObj = s3.deleteObject(deleteParams).promise();
|
|
promises.push(delObj);
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
} catch (error) {
|
|
if (error.code !== 'NoSuchBucket') {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const accelerateBucket = async (
|
|
s3: S3,
|
|
bucketName: string,
|
|
accelerated: boolean
|
|
): Promise<any> => {
|
|
try {
|
|
await s3
|
|
.putBucketAccelerateConfiguration({
|
|
AccelerateConfiguration: {
|
|
Status: accelerated ? 'Enabled' : 'Suspended',
|
|
},
|
|
Bucket: bucketName,
|
|
})
|
|
.promise();
|
|
} catch (e) {
|
|
if (e.code === 'NoSuchBucket') {
|
|
await utils.sleep(2000);
|
|
return accelerateBucket(s3, bucketName, accelerated);
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
export const deleteBucket = async (s3: S3, bucketName: string): Promise<any> => {
|
|
try {
|
|
await s3.deleteBucket({ Bucket: bucketName }).promise();
|
|
} catch (error) {
|
|
if (error.code !== 'NoSuchBucket') {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const configureCors = async (
|
|
s3: S3,
|
|
bucketName: string,
|
|
config: S3.CORSConfiguration
|
|
): Promise<any> => {
|
|
const params = { Bucket: bucketName, CORSConfiguration: config };
|
|
try {
|
|
await s3.putBucketCors(params).promise();
|
|
} catch (e) {
|
|
if (e.code === 'NoSuchBucket') {
|
|
await utils.sleep(2000);
|
|
return configureCors(s3, bucketName, config);
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
export const pathToPosix = (path: string): string => path.replace(/\\/g, '/');
|
|
|
|
export const readDirectoryFiles = (directory: string): Promise<Array<Item>> => {
|
|
const items: Item[] = [];
|
|
return new Promise((resolve, reject) => {
|
|
klaw(directory.trim())
|
|
.on('data', (item) => items.push(item))
|
|
.on('end', () => {
|
|
resolve(items);
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
};
|
|
|
|
export const filterOutDirectories = (fileItem: Item): boolean => !fileItem.stats.isDirectory();
|
|
|
|
export const getMimeType = (filePath: string): string =>
|
|
mime.lookup(path.basename(filePath)) || 'application/octet-stream';
|