@engine9/packet-tools
Version:
Tools for dealing with Engine9 packets
247 lines (216 loc) • 7.42 kB
JavaScript
const debug = require('debug')('S3Worker');
const fs = require('node:fs');
// eslint-disable-next-line import/no-unresolved
const { mimeType: mime } = require('mime-type/with-db');
const {
S3Client,
GetObjectCommand,
HeadObjectCommand,
GetObjectAttributesCommand, PutObjectCommand,
ListObjectsV2Command,
} = require('@aws-sdk/client-s3');
const { getTempFilename } = require('./tools');
function Worker() {}
function getParts(filename) {
if (!filename || filename.indexOf('s3://') !== 0) throw new Error(`Invalid filename for s3:${filename}`);
const parts = filename.split('/');
const Bucket = parts[2];
const Key = parts.slice(3).join('/');
return { Bucket, Key };
}
Worker.prototype.getClient = function () {
if (!this.client) this.client = new S3Client({});
return this.client;
};
Worker.prototype.getMetadata = async function ({ filename }) {
const s3Client = this.getClient();
const { Bucket, Key } = getParts(filename);
const resp = await s3Client.send(new GetObjectAttributesCommand({
Bucket,
Key,
ObjectAttributes: ['ETag', 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize'],
}));
return resp;
};
Worker.prototype.getMetadata.metadata = {
options: {
filename: {},
},
};
Worker.prototype.stream = async function ({ filename }) {
const s3Client = new S3Client({});
const { Bucket, Key } = getParts(filename);
const command = new GetObjectCommand({ Bucket, Key });
try {
debug(`Streaming file ${Key}`);
const response = await s3Client.send(command);
return { stream: response.Body };
} catch (e) {
debug(`Could not stream filename:${filename}`);
throw e;
}
};
Worker.prototype.stream.metadata = {
options: {
filename: {},
},
};
Worker.prototype.download = async function ({ filename }) {
const file = filename.split('/').pop();
const localPath = await getTempFilename({ targetFilename: file });
const s3Client = new S3Client({});
const { Bucket, Key } = getParts(filename);
const command = new GetObjectCommand({ Bucket, Key });
debug(`Downloading ${file} to ${localPath}`);
const response = await s3Client.send(command);
const fileStream = fs.createWriteStream(localPath);
response.Body.pipe(fileStream);
return new Promise((resolve, reject) => {
fileStream.on('finish', async () => {
const { size } = await fs.promises.stat(localPath);
resolve({ size, filename: localPath });
});
fileStream.on('error', reject);
});
};
Worker.prototype.download.metadata = {
options: {
filename: {},
},
};
Worker.prototype.put = async function (options) {
const { filename, directory } = options;
if (!filename) throw new Error('Local filename required');
if (directory?.indexOf('s3://') !== 0) throw new Error(`directory path must start with s3://, is ${directory}`);
const file = options.file || filename.split('/').pop();
const parts = directory.split('/');
const Bucket = parts[2];
const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
const Body = fs.createReadStream(filename);
const ContentType = mime.lookup(file);
debug(`Putting ${filename} to ${JSON.stringify({ Bucket, Key, ContentType })}}`);
const s3Client = new S3Client({});
const command = new PutObjectCommand({
Bucket, Key, Body, ContentType,
});
return s3Client.send(command);
};
Worker.prototype.put.metadata = {
options: {
filename: {},
directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
file: { description: 'Name of file, defaults to the filename' },
},
};
Worker.prototype.write = async function (options) {
const { directory, file, content } = options;
if (!directory?.indexOf('s3://') === 0) throw new Error('directory must start with s3://');
const parts = directory.split('/');
const Bucket = parts[2];
const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
const Body = content;
debug(`Writing content of length ${content.length} to ${JSON.stringify({ Bucket, Key })}}`);
const s3Client = new S3Client({});
const ContentType = mime.lookup(file);
const command = new PutObjectCommand({
Bucket, Key, Body, ContentType,
});
return s3Client.send(command);
};
Worker.prototype.write.metadata = {
options: {
directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
file: { description: 'Name of file, defaults to the filename' },
content: { description: 'Contents of file' },
},
};
Worker.prototype.list = async function ({ directory }) {
if (!directory) throw new Error('directory is required');
let dir = directory;
while (dir.slice(-1) === '/') dir = dir.slice(0, -1);
const { Bucket, Key: Prefix } = getParts(dir);
const s3Client = new S3Client({});
const command = new ListObjectsV2Command({
Bucket,
Prefix: `${Prefix}/`,
Delimiter: '/',
});
const { Contents: files, CommonPrefixes } = await s3Client.send(command);
// debug('Prefixes:', { CommonPrefixes });
const output = [].concat((CommonPrefixes || []).map((f) => ({
name: f.Prefix.slice(Prefix.length + 1, -1),
type: 'directory',
})))
.concat((files || []).map(({ Key }) => ({
name: Key.slice(Prefix.length + 1),
type: 'file',
})));
return output;
};
Worker.prototype.list.metadata = {
options: {
directory: { required: true },
},
};
/* List everything with the prefix */
Worker.prototype.listAll = async function ({ directory }) {
if (!directory) throw new Error('directory is required');
let dir = directory;
while (dir.slice(-1) === '/') dir = dir.slice(0, -1);
const { Bucket, Key: Prefix } = getParts(dir);
const s3Client = new S3Client({});
const files = [];
let ContinuationToken = null;
do {
const command = new ListObjectsV2Command({
Bucket,
Prefix: `${Prefix}/`,
ContinuationToken,
// Delimiter: '/',
});
debug(`Sending command with ContinuationToken ${ContinuationToken}`);
// eslint-disable-next-line no-await-in-loop
const result = await s3Client.send(command);
const newFiles = (result.Contents?.map((d) => `s3://${Bucket}/${d.Key}`) || []);
debug(`Retrieved ${newFiles.length} new files, total ${files.length},sample ${newFiles.slice(0, 3).join(',')}`);
files.push(...newFiles);
ContinuationToken = result.NextContinuationToken;
} while (ContinuationToken);
return files;
};
Worker.prototype.listAll.metadata = {
options: {
directory: { required: true },
},
};
Worker.prototype.stat = async function ({ filename }) {
if (!filename) throw new Error('filename is required');
const s3Client = new S3Client({});
const { Bucket, Key } = getParts(filename);
const command = new HeadObjectCommand({ Bucket, Key });
const response = await s3Client.send(command);
const {
// "AcceptRanges": "bytes",
ContentLength, // : "3191",
ContentType, // : "image/jpeg",
// ETag": "\"6805f2cfc46c0f04559748bb039d69ae\"",
LastModified, // : "2016-12-15T01:19:41.000Z",
// Metadata": {},
// VersionId": "null"
} = response;
const modifiedAt = new Date(LastModified);
const createdAt = modifiedAt;// Same for S3
const size = parseInt(ContentLength, 10);
return {
createdAt,
modifiedAt,
contentType: ContentType,
size,
};
};
Worker.prototype.stat.metadata = {
options: {
filename: {},
},
};
module.exports = Worker;