UNPKG

lemon-core

Version:
462 lines 20.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AWSS3Service = void 0; /** * `s3s-service.js` * - common S3 services. * * * @author Steve Jung <steve@lemoncloud.io> * @date 2019-07-19 initial version * @date 2019-11-26 cleanup and optimized for `lemon-core#v2` * @date 2023-02-08 support of `listObject()` * * @copyright (C) lemoncloud.io 2019 - All Rights Reserved. */ /** **************************************************************************************************************** * Common Headers ** ****************************************************************************************************************/ // eslint-disable-next-line @typescript-eslint/no-unused-vars const engine_1 = require("../../engine"); const NS = engine_1.$U.NS('S3', 'blue'); const client_s3_1 = require("@aws-sdk/client-s3"); // eslint-disable-next-line prettier/prettier const client_s3_2 = require("@aws-sdk/client-s3"); const path_1 = __importDefault(require("path")); const mime_types_1 = __importDefault(require("mime-types")); const uuid_1 = require("uuid"); const test_helper_1 = require("../../common/test-helper"); const stream_1 = require("stream"); const tools_1 = require("../../tools"); /** **************************************************************************************************************** * Public Instance Exported. ** ****************************************************************************************************************/ const region = () => engine_1.$engine.environ('REGION', 'ap-northeast-2'); /** * use `target` as value or environment value. * environ('abc') => string 'abc' * environ('ABC') => use `env.ABC` */ const environ = (target, defEnvName, defEnvValue) => { const isUpperStr = target && /^[A-Z][A-Z0-9_]+$/.test(target); defEnvName = isUpperStr ? target : defEnvName; const val = defEnvName ? engine_1.$engine.environ(defEnvName, defEnvValue) : defEnvValue; target = isUpperStr ? '' : target; return `${target || val}`; }; /** * get aws client for S3 */ const instance = () => { const cfg = (0, tools_1.awsConfig)(engine_1.$engine, region()); return new client_s3_1.S3Client(cfg); // SQS Instance. shared one??? }; /** * main service implement. */ class AWSS3Service { constructor() { /** * get name of this */ this.name = () => `S3`; /** * hello */ this.hello = () => `aws-s3-service:${this.bucket()}`; /** * get target endpoint by name. */ this.bucket = (target) => environ(target, AWSS3Service.ENV_S3_NAME, AWSS3Service.DEF_S3_BUCKET); /** * retrieve metadata without returning the object * * @param {string} key * @return metadata object / null if not exists */ this.headObject = (key) => __awaiter(this, void 0, void 0, function* () { var _a; if (!key) throw new Error(`@key (string) is required - headObject(${key !== null && key !== void 0 ? key : ''})`); const Bucket = this.bucket(); const params = { Bucket, Key: key }; // call s3.headObject. const s3 = instance(); try { const data = yield s3.send(new client_s3_2.HeadObjectCommand(params)); (0, engine_1._log)(NS, '> data =', engine_1.$U.json(Object.assign(Object.assign({}, data), { Contents: undefined }))); // const sample = { // AcceptRanges: 'bytes', // ContentLength: 47, // ContentType: 'application/json; charset=utf-8', // ETag: '"51f209a54902230ac3395826d7fa1851"', // Expiration: 'expiry-date="Mon, 10 Apr 2023 00:00:00 GMT", rule-id="delete-old-json"', // LastModified: '2023-02-08T14:53:12.000Z', // Metadata: { contenttype: 'application/json; charset=utf8', md5: '51f209a54902230ac3395826d7fa1851' }, // ServerSideEncryption: 'AES256', // }; const result = { ContentType: data.ContentType, ContentLength: data.ContentLength, Metadata: data.Metadata, ETag: data.ETag, LastModified: engine_1.$U.ts(data.LastModified), }; return result; } catch (e) { if (((_a = e === null || e === void 0 ? void 0 : e.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode) === 404 || e.name === 'NotFound') return null; (0, engine_1._err)(NS, '! err=', e); throw e; } }); /** * upload a file to S3 Bucket * * ```js * const res = $s3.putObject(JSON.stringify({ message }), 'test.json'); * // response would be like * { * "Bucket": "lemon-hello-www", * "ETag": "5e206.....8bd4c", * "Key": "test.json", * "Location": "https://lemon-hello-www.s3.ap-northeast-2.amazonaws.com/test.json", * } * ``` * * @param {string|Buffer} content content body * @param {string} key (optional) S3 key to put * @param {Metadata} metadata (optional) metadata to store * @param {object} tags (optional) tag set */ this.putObject = (content, key, metadata, tags) => __awaiter(this, void 0, void 0, function* () { if (!content) throw new Error(`@content (buffer) is required - putObject()`); const paramBuilder = new S3PutObjectRequestBuilder(this.bucket(), content); key && paramBuilder.setKey(key); metadata && paramBuilder.setMetadata(metadata); tags && paramBuilder.setTags(tags); const params = paramBuilder.asParams(); (0, engine_1._log)(NS, `> params.ContentType =`, params.ContentType); (0, engine_1._log)(NS, `> params.ContentLength =`, params.ContentLength); (0, engine_1._log)(NS, `> params.Metadata =`, params.Metadata); (0, engine_1._log)(NS, `> params.Tagging =`, params.Tagging); // call s3.upload() const s3 = instance(); try { const data = yield s3.send(new client_s3_2.PutObjectCommand(params)); delete data.key; // NOTE: remove undeclared property 'key' returned from aws-sdk const location = `https://${params.Bucket}.s3.${region()}.amazonaws.com/${params.Key}`; (0, engine_1._log)(NS, `> data[${params.Bucket}].Location =`, engine_1.$U.json(location)); const result = { Bucket: params.Bucket, Location: location, Key: params.Key, ETag: data.ETag, ContentType: params.ContentType, ContentLength: params.ContentLength, Metadata: params.Metadata, }; return result; } catch (e) { (0, engine_1._err)(NS, `! err[${params.Bucket}] =`, e); throw e; } }); /** * get a file from S3 Bucket * * @param {string} key */ this.getObject = (key) => __awaiter(this, void 0, void 0, function* () { if (!key) throw new Error(`@key (string) is required - getObject(${key !== null && key !== void 0 ? key : ''})`); const Bucket = this.bucket(); const params = { Bucket, Key: key }; //* call s3.getObject. const s3 = instance(); try { const data = yield s3.send(new client_s3_2.GetObjectCommand(params)); (0, engine_1._log)(NS, '> data.type =', typeof data); const { ContentType, ContentLength, Body, ETag, Metadata, TagCount } = data; // convert stream to buffer for readable stream. const buffer = Body && Body instanceof stream_1.Readable ? yield this.streamToBuffer(Body) : undefined; const result = { ContentType, ContentLength, Body: buffer, ETag, Metadata }; if (TagCount) result.TagCount = TagCount; return result; } catch (e) { (0, engine_1._err)(NS, '! err=', e); throw e; } }); /** * return decoded Object from bucket file. * * @param {string} key ex) 'hello-0001.json' , 'dist/hello-0001.json */ this.getDecodedObject = (key) => __awaiter(this, void 0, void 0, function* () { if (!key) throw new Error(`@key (string) is required - getDecodedObject(${key !== null && key !== void 0 ? key : ''})`); const Bucket = this.bucket(); const params = { Bucket, Key: key }; //* call s3.getObject. const s3 = instance(); try { const data = yield s3.send(new client_s3_2.GetObjectCommand(params)); (0, engine_1._log)(NS, '> data.type =', typeof data); const buffer = data.Body && data.Body instanceof stream_1.Readable ? yield this.streamToBuffer(data.Body) : undefined; const content = buffer === null || buffer === void 0 ? void 0 : buffer.toString(); return JSON.parse(content); } catch (e) { (0, engine_1._err)(NS, '! err=', e); throw e; } }); /** * get tag-set of object * * @param {string} key */ this.getObjectTagging = (key) => __awaiter(this, void 0, void 0, function* () { var _b; if (!key) throw new Error(`@key (string) is required - getObjectTagging(${key !== null && key !== void 0 ? key : ''})`); const Bucket = this.bucket(); const params = { Bucket, Key: key }; //* call s3.getObjectTagging. const s3 = instance(); try { const data = yield s3.send(new client_s3_2.GetObjectTaggingCommand(params)); (0, engine_1._log)(NS, `> data =`, engine_1.$U.json(data)); return (_b = data === null || data === void 0 ? void 0 : data.TagSet) === null || _b === void 0 ? void 0 : _b.reduce((tagSet, tag) => { const { Key, Value } = tag; tagSet[Key] = Value; return tagSet; }, {}); } catch (e) { (0, engine_1._err)(NS, '! err=', e); throw e; } }); /** * delete object from bucket * * @param {string} key */ this.deleteObject = (key) => __awaiter(this, void 0, void 0, function* () { if (!key) throw new Error(`@key (string) is required - deleteObject(${key !== null && key !== void 0 ? key : ''})`); const Bucket = this.bucket(); const params = { Bucket, Key: key }; //* call s3.deleteObject. const s3 = instance(); try { const data = yield s3.send(new client_s3_2.DeleteObjectCommand(params)); (0, engine_1._log)(NS, '> data =', engine_1.$U.json(data)); } catch (e) { (0, engine_1._err)(NS, '! err=', e); throw e; } }); /** * list objects in bucket */ this.listObjects = (options) => __awaiter(this, void 0, void 0, function* () { var _c, _d, _e, _f, _g, _h; // if (!key) throw new Error('@key is required!'); const Prefix = (_c = options === null || options === void 0 ? void 0 : options.prefix) !== null && _c !== void 0 ? _c : ''; const Delimiter = (_d = options === null || options === void 0 ? void 0 : options.delimiter) !== null && _d !== void 0 ? _d : '/'; const MaxKeys = Math.min((_e = options === null || options === void 0 ? void 0 : options.limit) !== null && _e !== void 0 ? _e : 10, 1000); const unlimited = (_f = options === null || options === void 0 ? void 0 : options.unlimited) !== null && _f !== void 0 ? _f : false; const nextToken = options === null || options === void 0 ? void 0 : options.nextToken; const throwable = (_g = options === null || options === void 0 ? void 0 : options.throwable) !== null && _g !== void 0 ? _g : true; //* build the req-params. const Bucket = this.bucket(); const params = { Bucket, Prefix, Delimiter, MaxKeys, }; if (nextToken) params.ContinuationToken = nextToken; //* call s3.listObjectsV2. const s3 = instance(); const result = { Contents: null, MaxKeys, KeyCount: 0, }; try { const data = yield s3.send(new client_s3_2.ListObjectsV2Command(params)); //INFO! - minimize log output.... (0, engine_1._log)(NS, '> data =', engine_1.$U.json(Object.assign(Object.assign({}, data), { Contents: undefined }))); (0, engine_1._log)(NS, '> data[0] =', engine_1.$U.json((_h = data === null || data === void 0 ? void 0 : data.Contents) === null || _h === void 0 ? void 0 : _h[0])); if (data) { result.Contents = data.Contents; result.MaxKeys = data.MaxKeys; result.KeyCount = data.KeyCount; result.IsTruncated = data.IsTruncated; result.NextContinuationToken = data.NextContinuationToken; } //* list all keys. if (unlimited) { while (result.IsTruncated) { //* fetch next list. const res2 = yield s3.send(new client_s3_2.ListObjectsV2Command(Object.assign(Object.assign({}, params), { ContinuationToken: result.NextContinuationToken }))); //* update contents. result.Contents = result.Contents.concat(0 ? res2.Contents.slice(1) : res2.Contents); result.IsTruncated = res2.IsTruncated; result.KeyCount += engine_1.$U.N(res2.KeyCount, 0); result.NextContinuationToken = res2.NextContinuationToken; } } } catch (e) { (0, engine_1._err)(NS, '! err=', e); if (throwable) throw e; result.error = (0, test_helper_1.GETERR)(e); } // returns. return result; }); /** * Convert a readable stream into a single Buffer. * Required in AWS SDK v3, as S3.getObject() returns a stream instead of a Buffer. * * Used for JSON parsing in getDecodedObject() and getObject(). */ this.streamToBuffer = (stream) => { var stream_2, stream_2_1; return __awaiter(this, void 0, void 0, function* () { var e_1, _a; const chunks = []; try { for (stream_2 = __asyncValues(stream); stream_2_1 = yield stream_2.next(), !stream_2_1.done;) { const chunk = stream_2_1.value; chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (stream_2_1 && !stream_2_1.done && (_a = stream_2.return)) yield _a.call(stream_2); } finally { if (e_1) throw e_1.error; } } return Buffer.concat(chunks); }); }; } } exports.AWSS3Service = AWSS3Service; /** * environ name to use `bucket` */ AWSS3Service.ENV_S3_NAME = 'MY_S3_BUCKET'; /** * default `bucket` name */ AWSS3Service.DEF_S3_BUCKET = 'lemon-hello-www'; /** * class `S3PutObjectRequestBuilder` * - util class to build S3.PutObjectRequest parameter */ class S3PutObjectRequestBuilder { /** * constructor */ constructor(bucket, content) { /** * guess content-type from filename * @param filename * @private */ this.getContentType = (filename) => { const extname = path_1.default.extname(filename); return mime_types_1.default.contentType(extname) || undefined; }; const buffer = typeof content === 'string' ? Buffer.from(content) : content; this.Body = buffer; this.Bucket = bucket; this.ContentLength = buffer.length; this.Metadata = { md5: engine_1.$U.md5(buffer, 'hex') }; } /** * explicitly set key * @param key S3 object key */ setKey(key) { this.Key = key; if (!this.Metadata['Content-Type']) { this.setMetadata({ 'Content-Type': this.getContentType(key) }); } return this; } /** * add metadata * @param metadata key-value dictionary (only string is allowed for values.) */ setMetadata(metadata) { if (metadata['Content-Type']) { this.ContentType = metadata['Content-Type']; delete metadata['Content-Type']; } else if (metadata.origin) { this.ContentType = this.getContentType(metadata.origin); } this.Metadata = Object.assign({ md5: this.Metadata.md5 }, metadata); // preserve 'md5' field return this; } /** * add tags * @param tags key-value dictionary (only string is allowed for values.) */ setTags(tags) { this.Tagging = new URLSearchParams(tags).toString(); return this; } /** * return PutObjectRequest object */ asParams() { const { Body, Bucket, ContentLength, Metadata, Tagging } = this; let { ContentType, Key } = this; // generate object key if not specified // - generate UUID filename // - get extension from content-type or use 'json' if (!Key) { const ext = (this.ContentType && mime_types_1.default.extension(this.ContentType)) || 'json'; Key = `${(0, uuid_1.v4)()}.${ext}`; } // generate content-type if not specified if (!ContentType) ContentType = this.getContentType(Key); return { Bucket, Key, Body, ContentLength, ContentType, Metadata, Tagging }; } } //# sourceMappingURL=aws-s3-service.js.map