From f683177370b464cb840c1aedee0f296c2bebfd7a Mon Sep 17 00:00:00 2001 From: Nhan Nguyen Date: Thu, 10 Feb 2022 10:14:07 +0700 Subject: [PATCH] Support single file upload --- .github/workflows/test.yml | 7 +++++++ README.md | 1 + action.yml | 3 +++ dist/index.js | 40 ++++++++++++++++++++++++++++++++++---- package-lock.json | 5 ++--- package.json | 2 +- src/httpClient.js | 37 ++++++++++++++++++++++++++++++++--- src/index.js | 3 ++- 8 files changed, 86 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1acd8ce..f8f8033 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,3 +61,10 @@ jobs: method: 'POST' data: '{ "key": "value" }' files: '{ "file": "${{ github.workspace }}/testfile.txt" }' + + - name: Request Postman Echo POST single file + uses: ./ + with: + url: 'https://postman-echo.com/post' + method: 'POST' + file: "${{ github.workspace }}/testfile.txt" diff --git a/README.md b/README.md index fa3acb2..14fae05 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ jobs: |contentType | Request ContentType| application/json | |data | Request Body Content:
- text content like JSON or XML
- key=value pairs separated by '&' and contentType: application/x-www-form-urlencoded

only for POST / PUT / PATCH Requests | '{}' | |files | Map of key / absolute file paths send as multipart/form-data request to the API, if set the contentType is set to multipart/form-data, values provided by data will be added as additional FormData values, nested objects are not supported. **Example provided in the _test_ Workflow of this Action** | '{}' | +|file | Single absolute file path send as `application/octet-stream` request to the API, if set the contentType is set to `application/octet-stream`. This input will be ignored if either `data` or `files` input is present. **Example provided in the _test_ Workflow of this Action** || |timeout| Request Timeout in ms | 5000 (5s) | |username| Username for Basic Auth || |password| Password for Basic Auth || diff --git a/action.yml b/action.yml index 91e4f0e..2a8bf95 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,9 @@ inputs: description: 'Map of absolute file paths as JSON String' required: false default: '{}' + file: + description: 'A single absolute file path' + required: false username: description: 'Auth Username' required: false diff --git a/dist/index.js b/dist/index.js index 028e115..fabeafc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1909,6 +1909,7 @@ const METHOD_POST = 'POST' * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} param0.instanceConfig * @param {string} param0.data Request Body as string, default {} * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} + * @param {string} param0.file Single request file (absolute path) * @param {{ username: string; password: string }|undefined} param0.auth Optional HTTP Basic Auth * @param {*} param0.actions * @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes @@ -1917,7 +1918,7 @@ const METHOD_POST = 'POST' * * @returns {void} */ -const request = async({ method, instanceConfig, data, files, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { +const request = async({ method, instanceConfig, data, files, file, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { try { if (escapeData) { data = data.replace(/"[^"]*"/g, (match) => { @@ -1930,8 +1931,8 @@ const request = async({ method, instanceConfig, data, files, auth, actions, igno } if (files && files !== '{}') { - filesJson = convertToJSON(files) - dataJson = convertToJSON(data) + let filesJson = convertToJSON(files) + let dataJson = convertToJSON(data) if (Object.keys(filesJson).length > 0) { try { @@ -1944,6 +1945,12 @@ const request = async({ method, instanceConfig, data, files, auth, actions, igno } } + // Only consider file if neither data nor files provided + if ((!data || data === '{}') && (!files || files === '{}') && file) { + data = fs.createReadStream(file) + updateConfigForFile(instanceConfig, file, actions) + } + const requestData = { auth, method, @@ -2041,6 +2048,30 @@ const updateConfig = async (instanceConfig, formData, actions) => { } } +/** + * @param instanceConfig + * @param filePath + * @param {*} actions + * + * @returns {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} + */ +const updateConfigForFile = (instanceConfig, filePath, actions) => { + try { + const { size } = fs.statSync(filePath) + + return { + ...instanceConfig, + headers: { + ...instanceConfig.headers, + 'Content-Length': size, + 'Content-Type': 'application/octet-stream' + } + } + } catch(error) { + actions.setFailed({ message: `Unable to read Content-Length: ${error.message}`, data, files }) + } +} + /** * @param {FormData} formData * @@ -4898,6 +4929,7 @@ const instanceConfig = { const data = core.getInput('data') || '{}'; const files = core.getInput('files') || '{}'; +const file = core.getInput('file') const method = core.getInput('method') || METHOD_POST; const preventFailureOnNoResponse = core.getInput('preventFailureOnNoResponse') === 'true'; const escapeData = core.getInput('escapeData') === 'true'; @@ -4909,7 +4941,7 @@ if (typeof ignoreStatusCodes === 'string' && ignoreStatusCodes.length > 0) { ignoredCodes = ignoreStatusCodes.split(',').map(statusCode => parseInt(statusCode.trim())) } -request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, ignoredCodes, actions: new GithubActions() }) +request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, file, ignoredCodes, actions: new GithubActions() }) /***/ }), diff --git a/package-lock.json b/package-lock.json index 2932195..579f103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,11 @@ { "name": "http-request-action", - "version": "1.8.0", + "version": "1.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "http-request-action", - "version": "1.8.0", + "version": "1.9.0", "license": "MIT", "dependencies": { "@zeit/ncc": "^0.22", diff --git a/package.json b/package.json index 85b025d..427edef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-request-action", - "version": "1.8.2", + "version": "1.9.0", "description": "", "main": "src/index.js", "private": false, diff --git a/src/httpClient.js b/src/httpClient.js index e35ee9d..c167ab6 100644 --- a/src/httpClient.js +++ b/src/httpClient.js @@ -11,6 +11,7 @@ const METHOD_POST = 'POST' * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} param0.instanceConfig * @param {string} param0.data Request Body as string, default {} * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} + * @param {string} param0.file Single request file (absolute path) * @param {{ username: string; password: string }|undefined} param0.auth Optional HTTP Basic Auth * @param {*} param0.actions * @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes @@ -19,7 +20,7 @@ const METHOD_POST = 'POST' * * @returns {void} */ -const request = async({ method, instanceConfig, data, files, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { +const request = async({ method, instanceConfig, data, files, file, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { try { if (escapeData) { data = data.replace(/"[^"]*"/g, (match) => { @@ -32,8 +33,8 @@ const request = async({ method, instanceConfig, data, files, auth, actions, igno } if (files && files !== '{}') { - filesJson = convertToJSON(files) - dataJson = convertToJSON(data) + let filesJson = convertToJSON(files) + let dataJson = convertToJSON(data) if (Object.keys(filesJson).length > 0) { try { @@ -46,6 +47,12 @@ const request = async({ method, instanceConfig, data, files, auth, actions, igno } } + // Only consider file if neither data nor files provided + if ((!data || data === '{}') && (!files || files === '{}') && file) { + data = fs.createReadStream(file) + updateConfigForFile(instanceConfig, file, actions) + } + const requestData = { auth, method, @@ -143,6 +150,30 @@ const updateConfig = async (instanceConfig, formData, actions) => { } } +/** + * @param instanceConfig + * @param filePath + * @param {*} actions + * + * @returns {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} + */ +const updateConfigForFile = (instanceConfig, filePath, actions) => { + try { + const { size } = fs.statSync(filePath) + + return { + ...instanceConfig, + headers: { + ...instanceConfig.headers, + 'Content-Length': size, + 'Content-Type': 'application/octet-stream' + } + } + } catch(error) { + actions.setFailed({ message: `Unable to read Content-Length: ${error.message}`, data, files }) + } +} + /** * @param {FormData} formData * diff --git a/src/index.js b/src/index.js index e119a8b..4b665b7 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,7 @@ const instanceConfig = { const data = core.getInput('data') || '{}'; const files = core.getInput('files') || '{}'; +const file = core.getInput('file') const method = core.getInput('method') || METHOD_POST; const preventFailureOnNoResponse = core.getInput('preventFailureOnNoResponse') === 'true'; const escapeData = core.getInput('escapeData') === 'true'; @@ -47,4 +48,4 @@ if (typeof ignoreStatusCodes === 'string' && ignoreStatusCodes.length > 0) { ignoredCodes = ignoreStatusCodes.split(',').map(statusCode => parseInt(statusCode.trim())) } -request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, ignoredCodes, actions: new GithubActions() }) +request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, file, ignoredCodes, actions: new GithubActions() })