diff --git a/CHANGELOG.md b/CHANGELOG.md index 8233450..ae23d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Change Log (vscode-deploy-reloaded) -## 0.10.0 (December 30th, 2017; shell commands) +## 0.10.0 (December 30th, 2017; shell commands and S3) * added `executeOnStartup` [setting](https://github.com/mkloubert/vscode-deploy-reloaded/wiki#settings--), which runs [shell commands on startup](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/execute_on_startup) * fixed use of [if](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/if) property in setting objects +* better handling of [credentials config](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/target_s3bucket#credentials) of [S3 target](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/target_s3bucket) ## 0.9.0 (December 30th, 2017; [Composer](https://getcomposer.org/)) diff --git a/src/clients/s3bucket.ts b/src/clients/s3bucket.ts index b369939..74b0eb8 100644 --- a/src/clients/s3bucket.ts +++ b/src/clients/s3bucket.ts @@ -19,9 +19,11 @@ import * as AWS from 'aws-sdk'; import * as deploy_clients from '../clients'; import * as deploy_files from '../files'; import * as deploy_helpers from '../helpers'; +import * as deploy_values from '../values'; import * as Enumerable from 'node-enumerable'; import * as i18 from '../i18'; import * as MimeTypes from 'mime-types'; +import * as OS from 'os'; import * as Path from 'path'; import * as Moment from 'moment'; @@ -61,11 +63,35 @@ export interface S3BucketOptions { */ readonly type?: string; }; + /** + * A custom function that provides scopes directories for relative paths. + */ + readonly directoryScopeProvider?: S3DirectoryScopeProvider; /** * A function that detects the ACL for a file * when uploading it. */ readonly fileAcl?: S3BucketFileAclDetector; + /** + * A function that provides values for a client. + */ + readonly valueProvider?: S3ValueProvider; +} + +/** + * A function that provides the scope directories for relative paths. + */ +export type S3DirectoryScopeProvider = () => string | string[] | PromiseLike; + +/** + * A function that provides values for use in settings for a client. + */ +export type S3ValueProvider = () => deploy_values.Value | deploy_values.Value[] | PromiseLike; + +interface SharedIniFileCredentialsOptions { + profile?: string + filename?: string + disableAssumeRole?: boolean } @@ -100,7 +126,86 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { super(); } - private createInstance(): AWS.S3 { + private async createInstance(): Promise { + const AWS_DIR = Path.resolve( + Path.join( + OS.homedir(), + '.aws' + ) + ); + + let directoryScopeProvider = this.options.directoryScopeProvider; + if (!directoryScopeProvider) { + directoryScopeProvider = () => []; + } + + const DIRECTORY_SCOPES = Enumerable.from( + deploy_helpers.asArray( + await Promise.resolve( directoryScopeProvider() ) + ) + ).select(s => { + return deploy_helpers.toStringSafe(s); + }).where(s => { + return !deploy_helpers.isEmptyString(s); + }).select(s => { + if (!Path.isAbsolute(s)) { + s = Path.join(AWS_DIR, s); + } + + return Path.resolve(s); + }).toArray(); + + if (DIRECTORY_SCOPES.length < 1) { + DIRECTORY_SCOPES.push( AWS_DIR ); // .aws by default + } + + let valueProvider = this.options.valueProvider; + if (!valueProvider) { + valueProvider = () => []; + } + + const VALUES = deploy_helpers.asArray( + await Promise.resolve( valueProvider() ) + ); + + const REPLACE_WITH_VALUES = (val: any) => { + return deploy_values.replaceWithValues( + VALUES, + val, + ); + }; + + const FIND_FULL_FILE_PATH = async (p: string): Promise => { + p = deploy_helpers.toStringSafe(p); + + if (Path.isAbsolute(p)) { + // exist if file exists + + if (await deploy_helpers.exists(p)) { + if ((await deploy_helpers.lstat(p)).isFile()) { + return Path.resolve(p); // file exists + } + } + } + else { + // detect existing, full path + for (const DS of DIRECTORY_SCOPES) { + let fullPath = REPLACE_WITH_VALUES(p); + fullPath = Path.join(DS, fullPath); + fullPath = Path.resolve(fullPath); + + if (await deploy_helpers.exists(fullPath)) { + if ((await deploy_helpers.lstat(fullPath)).isFile()) { + return fullPath; // file found + } + } + } + } + + throw new Error(i18.t('fileNotFound', + p)); + }; + let bucket = deploy_helpers.toStringSafe(this.options.bucket).trim(); if ('' === bucket) { bucket = 'vscode-deploy-reloaded'; @@ -116,6 +221,65 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { } credentialConfig = this.options.credentials.config; + + switch (credentialType) { + case 'environment': + // EnvironmentCredentials + if (!deploy_helpers.isNullOrUndefined(credentialConfig)) { + credentialConfig = REPLACE_WITH_VALUES(credentialConfig).trim(); + } + break; + + case 'file': + // FileSystemCredentials + if (!deploy_helpers.isNullOrUndefined(credentialConfig)) { + credentialConfig = deploy_helpers.toStringSafe(credentialConfig); + + if (!deploy_helpers.isEmptyString(credentialConfig)) { + credentialConfig = await FIND_FULL_FILE_PATH(credentialConfig); + } + } + break; + + case 'shared': + // SharedIniFileCredentials + { + const GET_PROFILE_SAFE = (profile: any): string => { + profile = deploy_helpers.toStringSafe( + REPLACE_WITH_VALUES(profile) + ).trim(); + if ('' === profile) { + profile = undefined; + } + + return profile; + }; + + let sharedCfg: string | SharedIniFileCredentialsOptions = deploy_helpers.cloneObject( + credentialConfig + ); + if (deploy_helpers.isObject(sharedCfg)) { + sharedCfg.filename = deploy_helpers.toStringSafe(sharedCfg.filename); + } + else { + sharedCfg = { + profile: deploy_helpers.toStringSafe(sharedCfg), + }; + } + + if (deploy_helpers.isEmptyString(sharedCfg.filename)) { + sharedCfg.filename = undefined; + } + else { + sharedCfg.filename = await FIND_FULL_FILE_PATH(sharedCfg.filename); + } + + sharedCfg.profile = GET_PROFILE_SAFE(sharedCfg.profile); + + credentialConfig = sharedCfg; + } + break; + } } if (!credentialClass) { @@ -138,11 +302,11 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { path = toS3Path(path); - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); try { - const S3 = ME.createInstance(); + const S3 = await ME.createInstance(); const PARAMS: any = { Key: path, @@ -169,11 +333,11 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { path = toS3Path(path); - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); try { - const S3 = ME.createInstance(); + const S3 = await ME.createInstance(); const PARAMS: any = { Key: path, @@ -208,7 +372,7 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { path = toS3Path(path); - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); const ALL_OBJS: AWS.S3.Object[] = []; @@ -280,7 +444,7 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { }; try { - const S3 = ME.createInstance(); + const S3 = await ME.createInstance(); let currentContinuationToken: string | false = false; @@ -348,11 +512,11 @@ export class S3BucketClient extends deploy_clients.AsyncFileListBase { data = Buffer.alloc(0); } - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); try { - const S3 = ME.createInstance(); + const S3 = await ME.createInstance(); let contentType = MimeTypes.lookup( Path.basename(path) ); if (false === contentType) { diff --git a/src/i18.ts b/src/i18.ts index bedd3e9..6f947a1 100644 --- a/src/i18.ts +++ b/src/i18.ts @@ -146,6 +146,7 @@ export interface Translation { initializing?: string; }; file?: string; + fileNotFound?: string; files?: string; ftp?: { couldNotConnect?: string; diff --git a/src/lang/de.ts b/src/lang/de.ts index 5953bed..cc3fb65 100644 --- a/src/lang/de.ts +++ b/src/lang/de.ts @@ -139,6 +139,7 @@ export const translation: Translation = { initializing: "Erweiterung wird initialisiert ...", }, file: "Datei", + fileNotFound: "Datei{0:trim,surround,leading_space} nicht gefunden!", files: "Dateien", ftp: { couldNotConnect: "Konnte keine Verbindung aufbauen!", diff --git a/src/lang/en.ts b/src/lang/en.ts index e4239bb..5e95fe9 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -139,6 +139,7 @@ export const translation: Translation = { initializing: "Extension is initializing ...", }, file: "File", + fileNotFound: "File{0:trim,surround,leading_space} not found!", files: "Files", ftp: { couldNotConnect: "Could not start connection!", diff --git a/src/plugins/s3bucket.ts b/src/plugins/s3bucket.ts index 39481c0..f649b73 100644 --- a/src/plugins/s3bucket.ts +++ b/src/plugins/s3bucket.ts @@ -22,6 +22,8 @@ import * as deploy_helpers from '../helpers'; import * as deploy_log from '../log'; import * as deploy_plugins from '../plugins'; import * as deploy_targets from '../targets'; +import * as OS from 'os'; +import * as Path from 'path'; interface S3BucketContext extends deploy_plugins.AsyncFileClientPluginContext { + return SCOPES; + }, fileAcl: (file, defAcl) => { for (const ACL in FILTERS) { if (deploy_helpers.checkIfDoesMatchByFileFilter('/' + file, @@ -111,6 +129,9 @@ class S3BucketPlugin extends deploy_plugins.AsyncFileClientPluginBase { + return target.__workspace.getValues(); } }), getDir: (subDir) => {