diff --git a/README.md b/README.md index c756a6b..db15c0f 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,11 @@ ``` 4. After these steps your `angular.json` is updated with a new builder: + ```json "deploy": { - "builder": "@jefiozie/ngx-aws-deploy:deploy", - "options": {} + "builder": "@jefiozie/ngx-aws-deploy:deploy", + "options": {} } ``` @@ -62,7 +63,42 @@ npx cross-env NG_DEPLOY_AWS_ACCESS_KEY_ID=1234 NG_DEPLOY_AWS_SECRET_ACCESS_KEY=3 npx cross-env ... NG_DEPLOY_AWS_CF_DISTRIBUTION_ID=1234 ... ng deploy ``` -7. Run `ng deploy` to deploy your application to Amazon S3. +7. To apply properties on uploaded files, use the `NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST` variable or the target option `globFileUploadParamsList` as shown below. + We use an array of objects to represent the different configurations. + Each config object needs a `glob` property to define on which files the params will be set. + Other properties on the config object are the params to apply. + For informations about the possible params (ACL, Bucket, CacheControl, etc), checkout the documentation of the `s3.upload` method (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property) + +```json +"deploy": { + "builder": "@jefiozie/ngx-aws-deploy:deploy", + "options": { + "globFileUploadParamsList": [ + { + "glob": "*", + "ACL": "public-read", + "CacheControl": "max-age=3600" + }, + { + "glob": "*.html", + "CacheControl": "max-age=300" + } + ] + } +}, +``` + +The order of the config objects matters, the one that comes last is the one that will be used, as shown above: + +- CacheControl is set for all files but it's overidden by the next config object only for html files. Convenient for declaring exceptions and not repeat global settings. + +To set this using the environment variable `NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST`, simply stringify the config object to JSON with `JSON.stringify`. + +```bash +npx cross-env ... NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST='[{"glob":"*","ACL":"public-read","CacheControl":"max-age=3600"},{"glob":"*.html","CacheControl":"max-age=300"}]' ... ng deploy +``` + +8. Run `ng deploy` to deploy your application to Amazon S3. 🚀**_Happy deploying!_** 🚀 diff --git a/libs/ngx-aws-deploy/src/lib/deploy/config.ts b/libs/ngx-aws-deploy/src/lib/deploy/config.ts index 3737af2..79cb221 100644 --- a/libs/ngx-aws-deploy/src/lib/deploy/config.ts +++ b/libs/ngx-aws-deploy/src/lib/deploy/config.ts @@ -1,4 +1,4 @@ -import { Schema } from './schema'; +import { GlobFileUploadParamsList, Schema } from './schema'; export const getAccessKeyId = (): string => { return ( @@ -50,3 +50,28 @@ export const gets3ForcePathStyle = (): boolean => { export const getAwsEndpoint = (): string => { return process.env.AWS_ENDPOINT; }; + +const validateGlobFileUploadParamsList = ( + paramsList: GlobFileUploadParamsList +) => Array.isArray(paramsList) && !paramsList.some((params) => !params.glob); + +export const getGlobFileUploadParamsList = ( + builderConfig: Schema +): GlobFileUploadParamsList => { + let globFileUploadParamsList = []; + try { + globFileUploadParamsList = process.env + .NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST + ? JSON.parse(process.env.NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST) + : builderConfig.globFileUploadParamsList || []; + } catch (e) { + console.error( + 'Invalid JSON for NG_DEPLOY_AWS_GLOB_FILE_UPLOAD_PARAMS_LIST', + e + ); + } + + return validateGlobFileUploadParamsList(globFileUploadParamsList) + ? globFileUploadParamsList + : []; +}; diff --git a/libs/ngx-aws-deploy/src/lib/deploy/index.ts b/libs/ngx-aws-deploy/src/lib/deploy/index.ts index 5052922..b9b80e6 100644 --- a/libs/ngx-aws-deploy/src/lib/deploy/index.ts +++ b/libs/ngx-aws-deploy/src/lib/deploy/index.ts @@ -44,13 +44,21 @@ export default createBuilder( targetString += `:${context.target.configuration}`; } - const { bucket, region, subFolder } = await context.getTargetOptions( - targetFromTargetString(targetString) - ); + const { + bucket, + region, + subFolder, + globFileUploadParamsList, + } = await context.getTargetOptions(targetFromTargetString(targetString)); - const deployConfig = { bucket, region, subFolder } as Pick< + const deployConfig = { + bucket, + region, + subFolder, + globFileUploadParamsList, + } as Pick< Schema, - 'bucket' | 'region' | 'subFolder' + 'bucket' | 'region' | 'subFolder' | 'globFileUploadParamsList' >; let buildResult: BuilderOutput; @@ -82,8 +90,10 @@ export default createBuilder( context.logger.info(`✔ Build Completed`); } if (buildResult.success) { - const filesPath = buildResult.outputPath as string; - const files = getFiles(filesPath); + const { outputPath } = await context.getTargetOptions( + targetFromTargetString(buildTarget.name) + ); + const files = getFiles(`${outputPath}`); if (files.length === 0) { throw new Error( @@ -107,7 +117,7 @@ export default createBuilder( } context.logger.info('Start uploading files...'); - const success = await uploader.upload(files, filesPath); + const success = await uploader.upload(files, `${outputPath}`); if (success) { context.logger.info('✔ Finished uploading files...'); diff --git a/libs/ngx-aws-deploy/src/lib/deploy/schema.d.ts b/libs/ngx-aws-deploy/src/lib/deploy/schema.d.ts index 7e2a9fa..a52e9ea 100644 --- a/libs/ngx-aws-deploy/src/lib/deploy/schema.d.ts +++ b/libs/ngx-aws-deploy/src/lib/deploy/schema.d.ts @@ -1,3 +1,5 @@ +export type GlobFileUploadParams = { glob: string } & PutObjectRequest; +export type GlobFileUploadParamsList = Array; export interface Schema { configuration?: string; buildTarget?: string; @@ -11,4 +13,5 @@ export interface Schema { cfDistributionId?: string; deleteAfterUpload?: boolean; deleteBeforeUpload?: boolean; + globFileUploadParamsList?: GlobFileUploadParamsList; } diff --git a/libs/ngx-aws-deploy/src/lib/deploy/schema.json b/libs/ngx-aws-deploy/src/lib/deploy/schema.json index 9b2de11..5476521 100644 --- a/libs/ngx-aws-deploy/src/lib/deploy/schema.json +++ b/libs/ngx-aws-deploy/src/lib/deploy/schema.json @@ -59,7 +59,10 @@ "deleteBeforeUpload": { "type": "boolean", "default": false, - "description": "Remove all files in the bucket before uploading." + "description": "Remove all files in the bucket before uploading.", + "globFileUploadParamsList": { + "type": "array", + "description": "An array of objects representing params to pass to the s3.upload method (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property). In addition to the params, each object has a glob property allowing to target params to specific files." } } } diff --git a/libs/ngx-aws-deploy/src/lib/deploy/uploader.ts b/libs/ngx-aws-deploy/src/lib/deploy/uploader.ts index 2a90740..732c99b 100644 --- a/libs/ngx-aws-deploy/src/lib/deploy/uploader.ts +++ b/libs/ngx-aws-deploy/src/lib/deploy/uploader.ts @@ -1,14 +1,24 @@ import { BuilderContext } from '@angular-devkit/architect'; import * as AWS from 'aws-sdk'; -import { HeadBucketRequest, ObjectIdentifierList, PutObjectRequest } from 'aws-sdk/clients/s3'; +import { + HeadBucketRequest, + ObjectIdentifierList, + PutObjectRequest, +} from 'aws-sdk/clients/s3'; import * as mimeTypes from 'mime-types'; import * as fs from 'fs'; import * as path from 'path'; -import { Schema } from './schema'; +import * as minimatch from 'minimatch'; +import { + GlobFileUploadParams, + GlobFileUploadParamsList, + Schema, +} from './schema'; import { getAccessKeyId, getAwsEndpoint, getBucket, + getGlobFileUploadParamsList, getRegion, gets3ForcePathStyle, getSecretAccessKey, @@ -23,6 +33,7 @@ export class Uploader { private _region: string; private _subFolder: string; private _builderConfig: Schema; + private _globFileUploadParamsList: GlobFileUploadParamsList; constructor(context: BuilderContext, builderConfig: Schema) { this._context = context; @@ -30,6 +41,9 @@ export class Uploader { this._bucket = getBucket(this._builderConfig); this._region = getRegion(this._builderConfig); this._subFolder = getSubFolder(this._builderConfig); + this._globFileUploadParamsList = getGlobFileUploadParamsList( + this._builderConfig + ); AWS.config.update({ region: this._region }); @@ -86,6 +100,19 @@ export class Uploader { public async uploadFile(localFilePath: string, originFilePath: string) { const fileName = path.basename(localFilePath); + const globFileUploadParamsForFile = this._globFileUploadParamsList.filter( + (params: GlobFileUploadParams) => minimatch(originFilePath, params.glob) + ); + + const mergedParamsForFile = globFileUploadParamsForFile.reduce( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (acc, { glob, ...params }) => ({ + ...acc, + ...params, + }), + {} + ); + const body = fs.createReadStream(localFilePath); body.on('error', function (err) { console.log('File Error', err); @@ -98,6 +125,7 @@ export class Uploader { : originFilePath, Body: body, ContentType: mimeTypes.lookup(fileName) || undefined, + ...mergedParamsForFile, }; await this._s3 @@ -116,8 +144,12 @@ export class Uploader { public async deleteStaleFiles(localFiles: string[]) { const remoteFiles = await this.listObjectKeys(); - const staleFiles = this._subFolder ? localFiles.map((file) => `${this._subFolder}/${file}`) : localFiles; - const filesToDelete = remoteFiles.filter((file) => !staleFiles.includes(file.Key)); + const staleFiles = this._subFolder + ? localFiles.map((file) => `${this._subFolder}/${file}`) + : localFiles; + const filesToDelete = remoteFiles.filter( + (file) => !staleFiles.includes(file.Key) + ); return this.deleteFiles(filesToDelete); }