serverless-finch
Version:
Deploy your serverless static website to AWS S3.
176 lines (150 loc) • 5.03 kB
JavaScript
;
const fs = require('fs');
const mime = require('mime');
const minimatch = require('minimatch');
const path = require('path');
const getFileList = require('./get-file-list');
/**
* Uploads client files to an S3 bucket
* @param {Object} aws - AWS class
* @param {string} bucketName - Name of bucket to be configured
* @param {string} clientRoot - Full path to the root directory of client files
* @param {Object[]} headerSpec - Array of header values to add to files
* @param {string[]} orderSpec - Array of regex's to order upload by
* @param {string} keyPrefix - A prefix for all files uploaded
* @param {string} sse - ServerSideEncryption, AES256 | aws:kms | null
*/
function uploadDirectory(aws, bucketName, clientRoot, headerSpec, orderSpec, keyPrefix, sse) {
const allFiles = getFileList(clientRoot);
const filesGroupedByOrder = groupFilesByOrder(allFiles, orderSpec);
return filesGroupedByOrder.reduce((existingUploads, files) => {
return existingUploads.then(existingResults => {
const uploadList = buildUploadList(files, clientRoot, headerSpec, keyPrefix);
return Promise.all(
uploadList.map(u => uploadFile(aws, bucketName, u.filePath, u.fileKey, u.headers, sse))
).then(currentResults => existingResults.concat(currentResults));
});
}, Promise.resolve([]));
}
/**
* Uploads a file to an S3 bucket
* @param {Object} aws - AWS class
* @param {string} bucketName - Name of bucket to be configured
* @param {string} filePath - Full path to file to be uploaded
* @param {string} fileKey -
* @param {string} headers -
* @param {string} sse - ServerSideEncryption, AES256 | aws:kms | null
*/
function uploadFile(aws, bucketName, filePath, fileKey, headers, sse) {
const baseHeaderKeys = [
'Cache-Control',
'Content-Disposition',
'Content-Encoding',
'Content-Language',
'Content-Type',
'Expires',
'Website-Redirect-Location'
];
const fileBuffer = fs.readFileSync(filePath);
const params = {
Bucket: bucketName,
Key: fileKey,
Body: fileBuffer,
ContentType: mime.getType(filePath)
};
if (sse) {
params.ServerSideEncryption = sse;
}
Object.keys(headers).forEach(h => {
if (baseHeaderKeys.includes(h)) {
params[h.replace('-', '')] = headers[h];
} else {
if (!params.Metadata) {
params.Metadata = {};
}
params.Metadata[h] = headers[h];
}
});
return aws.request('S3', 'putObject', params);
}
function buildUploadList(files, clientRoot, headerSpec, keyPrefix) {
clientRoot = path.normalize(clientRoot);
if (!clientRoot.endsWith(path.sep)) {
clientRoot += path.sep;
}
const prefix = keyPrefix ? keyPrefix.split(path.sep) : [];
return files.map(f => {
const filePath = path.normalize(f);
const fileRelPath = filePath.replace(clientRoot, '');
const fileKey = path.normalize(fileRelPath).split(path.sep);
if (prefix.length) {
fileKey.unshift(...prefix);
}
const upload = {
filePath: filePath,
fileKey: fileKey.join('/'),
headers: {}
};
if (!headerSpec) {
return upload;
}
// add bucket-wide headers
if (headerSpec.ALL_OBJECTS) {
headerSpec.ALL_OBJECTS.forEach(h => {
upload.headers[h.name] = h.value;
});
}
// add headers by glob pattern
Object.keys(headerSpec)
.filter(s => !!s.match(/[*?[\]]/)) // Potential glob pattern
.filter(s => minimatch(fileRelPath, s, { matchBase: true }))
.sort((a, b) => a.length - b.length) // sort by length ascending
.forEach(s => {
headerSpec[s].forEach(h => {
upload.headers[h.name] = h.value;
});
});
// add folder-level headers
Object.keys(headerSpec)
.filter(s => s.endsWith(path.sep)) // folders
.sort((a, b) => a.length > b.length) // sort by length ascending
.forEach(s => {
if (fileRelPath.startsWith(path.normalize(s))) {
headerSpec[s].forEach(h => {
upload.headers[h.name] = h.value;
});
}
});
// add file-specific headers
Object.keys(headerSpec)
.filter(s => {
return path.normalize(s) === fileRelPath;
})
.forEach(s => {
headerSpec[s].forEach(h => {
upload.headers[h.name] = h.value;
});
});
return upload;
});
}
function groupFilesByOrder(files, orderSpec) {
if (!orderSpec) {
return [files];
}
const orderedFiles = files.map(f => ({ file: f, order: 0 }));
orderSpec.forEach((orderRegex, i) => {
const re = new RegExp(orderRegex, 'i');
orderedFiles.forEach(f => {
if (re.test(f.file)) {
f.order = i + 1;
}
});
});
const unmatchedFiles = orderedFiles.filter(f => f.order === 0).map(f => f.file);
const matchedFiles = orderSpec.map((orderRegex, i) =>
orderedFiles.filter(f => f.order === i + 1).map(f => f.file)
);
return [unmatchedFiles].concat(matchedFiles);
}
module.exports = uploadDirectory;