blobby-client
Version:
Blobby is an HTTP Proxy for Blob storage systems (such as S3) that automatically
120 lines (101 loc) • 4.9 kB
JavaScript
const retry = require('./retry');
module.exports = async (client, storage, fileKey, file, opts = {}) => {
const { waitForReplicas, headers = {}, contentType = 'application/octet-stream' } = opts;
if (!file.headers) file.headers = {};
// use request content-type if known, otherwise try to auto-detect
const headerContentType = headers['content-type'];
// use content-type provided by header if explicit enough, otherwise use format detected by extension
const ContentType = (headerContentType && headerContentType !== 'binary/octet-stream' && headerContentType !== 'application/x-www-form-urlencoded') ? headerContentType : contentType;
const ETag = headers['etag'];
const LastModified = headers['last-modified'];
// use request cache-control if avail, otherwise fallback storage setting
const CacheControl = headers['cache-control'] || storage.config.cacheControl;
const AccessControl = headers['x-amz-acl'] || storage.config.accessControl || 'public-read';
const CopySource = headers['x-amz-copy-source'];
const CopyAndReplace = CopySource && headers['x-amz-metadata-directive'] === 'REPLACE';
const copySourceSplit = CopySource && CopySource.split(':');
const SourceBucket = (copySourceSplit && copySourceSplit.length >= 2 && copySourceSplit[0]) || null;
const sourceKey = copySourceSplit && copySourceSplit[copySourceSplit.length - 1];
const isCopySupported = CopySource
&& typeof storage.copy === 'function'
&& (!storage.config.replicas || storage.config.replicas.reduce((state, r) => !state ? false : typeof storage.copy === 'function', true))
; // all-or-nothing native copy support
const CustomHeaders = {};
Object.keys(headers).forEach(k => {
const xHeader = /^x\-(.*)$/.exec(k);
if (xHeader && k !== 'x-amz-acl') {
CustomHeaders[xHeader[1]] = headers[k]; // forward custom headers
}
});
const fileInfo = { ContentType, CacheControl, AccessControl, CustomHeaders, bucket: SourceBucket, CopyAndReplace }; // storage file headers
if (ETag) fileInfo.ETag = ETag;
if (LastModified) fileInfo.LastModified = LastModified;
let copyFile;
if (sourceKey) { // if source is provided, attempt a copy
if (isCopySupported) { // native copy support comes later
copyFile = { headers: fileInfo };
} else {
let decodedSourceKey;
try {
decodedSourceKey = sourceKey.split('/').map(decodeURIComponent).join('/');
} catch (ex /* ignore */) {
decodedSourceKey = sourceKey;
}
const [headers, buffer] = await storage.fetch(decodedSourceKey, { acl: 'private' }).catch(err => {
err.statusCode = 404;
throw err;
});
headers.bucket = SourceBucket;
copyFile = { headers, buffer };
}
} else if (!Buffer.isBuffer(file.buffer)) {
throw new Error('Cannot write file without `buffer`');
} else {
file.headers = fileInfo;
}
let op = sourceKey && storage.copy
? storage.copy.bind(storage, sourceKey, fileKey, copyFile.headers)
: storage.store.bind(storage, fileKey, copyFile ? copyFile : file, {})
;
// always initiate write to master first
const writeMasterPromise = retry(op, client.config.retry).then(headers => {
const finalHeaders = headers || (copyFile ? copyFile.headers : file.headers) || {};
if (!finalHeaders.ETag && ETag) {
finalHeaders.ETag = ETag; // not all clients provide ETag on write
}
finalHeaders.ContentType = finalHeaders.ContentType || ContentType;
return finalHeaders;
});
if (Array.isArray(storage.config.replicas) && storage.config.replicas.length > 0) {
try {
const replicaTasks = storage.config.replicas.map(replica => writeToReplica(client, replica, sourceKey, fileKey, copyFile ? copyFile : file, opts));
if (waitForReplicas !== false) {
await Promise.all(replicaTasks);
} // else do not wait
} catch (ex) {
throw ex;
}
}
// let the caller wait on promise
return writeMasterPromise;
}
async function writeToReplica(client, replica, sourceKey, destinationKey, file, opts) {
const { headers = {} } = opts;
const replicaSplit = replica.split('::');
const configId = replicaSplit.length > 1 ? replicaSplit[0] : null;
const storageId = replicaSplit.length > 1 ? replicaSplit[1] : replicaSplit[0];
let config;
if (configId) {
config = await client.getConfig(configId);
} else {
config = client.config;
}
const storage = client.getStorage(storageId, config);
// use caching provided by the specific environment storage config
file.headers.CacheControl = headers['cache-control'] || storage.config.cacheControl;
const op = sourceKey && storage.copy // use copy if requested and supported
? storage.copy.bind(storage, sourceKey, destinationKey, file.headers)
: storage.store.bind(storage, destinationKey, file, {})
;
return retry(op, config.retry);
}