diff --git a/packages/InkBackend/InkBackend.js b/packages/InkBackend/InkBackend.js index 02820420d75dcaad6dad7712c7a0501313516406..af30d3c8319ab28fe7155a440edb8a7729801a14 100644 --- a/packages/InkBackend/InkBackend.js +++ b/packages/InkBackend/InkBackend.js @@ -1,214 +1,172 @@ -const config = require('config') -const rp = require('request-promise-native') -const Busboy = require('busboy') -const temp = require('temp').track() const fs = require('fs') const path = require('path') -const promiseRetry = require('promise-retry') const logger = require('@pubsweet/logger') +const Busboy = require('busboy') +const config = require('config') +const rp = require('request-promise-native') +const temp = require('temp') + +// rp.debug = true + +const inkConfig = config.get('pubsweet-component-ink-backend') -let inkConfig = config.get('pubsweet-component-ink-backend') -let inkEndpoint = inkConfig.inkEndpoint -let email = inkConfig.email -let password = inkConfig.password -let maxRetries = inkConfig.maxRetries || 60 +// Generate the absolute URL +const inkUrl = path => inkConfig.inkEndpoint + 'api/' + path -let authRequest = { - uri: inkEndpoint + '/api/auth/sign_in', +// Sign in +const authorize = () => rp({ method: 'POST', - body: { - email: email, - password: password + uri: inkUrl('auth/sign_in'), + formData: { + email: inkConfig.email, + password: inkConfig.password }, - json: true, headers: { 'Accept': 'application/vnd.ink.1' }, resolveWithFullResponse: true -} +}).then(res => ({ + 'client': res.headers['client'], + 'access-token': res.headers['access-token'] +})) -// Get an access token -let getAuth = () => { - return rp(authRequest).then(response => { - return { - accessToken: response.headers['access-token'], - client: response.headers['client'] - } - }).catch( - err => { - logger.error('INK API LOGIN FAILURE:', err) - throw err - } - ) -} +// Upload file to INK and execute the recipe +const upload = (recipeId, inputFile, auth) => rp({ + method: 'POST', + uri: inkUrl('recipes/' + recipeId + '/execute'), + headers: { + uid: inkConfig.email, + ...auth + }, + formData: { + input_files: [inputFile] + }, + json: true, + timeout: 60 * 60 * 1000 // 3600 seconds +}) -let defaultRecipeId = null -let recipeListUrl = inkEndpoint + '/api/recipes' +// Download the output file (keep trying if there's a 404 response, until it's ready) +const download = async (chain, auth, outputFileName) => { + const manifest = chain.input_file_manifest -const getRecipeId = () => { - if (defaultRecipeId) return Promise.resolve(defaultRecipeId) + if (manifest.length === 0) { + throw new Error('The INK server gave a malformed response (no input files in the process chain)') + } - const listRequest = auth => ({ - method: 'GET', - uri: recipeListUrl, - headers: { - 'uid': email, - 'access-token': auth.accessToken, - 'client': auth.client - } - }) + const interval = inkConfig.interval || 1000 // try once per second - return getAuth().then( - auth => rp(listRequest(auth)) - ).then( - response => Promise.resolve(JSON.parse(response)) - ).then( - response => { - const defaultRecipe = response.recipes.find( - recipe => recipe.name === 'Editoria Typescript' // XSweet recipe - ) - if (!defaultRecipe) throw new Error('could not get default recipe from INK') - defaultRecipeId = defaultRecipe.id - return Promise.resolve(defaultRecipeId) - } - ) -} + const maxRetries = inkConfig.maxRetries || 300 // retry for up to 5 minutes -getRecipeId() - -const inkRecipeUrl = () => getRecipeId().then( - recipeId => Promise.resolve(inkEndpoint + '/api/recipes/' + recipeId + '/execute') -) - -const healthCheckRequest = auth => inkRecipeUrl().then( - recipeUrl => { - const opts = { - uri: recipeUrl, - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'POST', - 'Access-Control-Request-Headers': 'access-token, client, expiry, token-type, uid', - 'Origin': 'http://ink.coko.foundation', - 'access-token': auth.accessToken, - 'client': auth.client - } - } - return Promise.resolve(opts) - } -) - -const uploadRequest = (data, auth) => inkRecipeUrl().then( - recipeUrl => { - const opts = { - uri: recipeUrl, - method: 'POST', - headers: { - 'uid': email, - 'access-token': auth.accessToken, - 'client': auth.client - }, - formData: { - input_files: [data] - } - } - return Promise.resolve(opts) - } -) + const uri = inkUrl('process_chains/' + chain.id + '/download_output_file') -// Upload file to INK and execute the recipe -const uploadToInk = data => auth => uploadRequest( - data, auth -).then( - rp -).then( - response => Promise.resolve([auth, JSON.parse(response)]) -) - -// Check if INK is alive and well -const checkInk = auth => healthCheckRequest(auth).then( - rp -).then( - response => { - return Promise.resolve(auth) + const qs = { + relative_path: outputFileName || path.basename(manifest[0].path, '.docx') + '.html' } -).catch( - err => { - throw err + + const headers = { + uid: inkConfig.email, + ...auth } -) - -const retryFor30SecondsUntil200 = (uri, auth) => { - const downloadRequest = { - method: 'GET', - uri: uri, - headers: { - 'uid': email, - 'access-token': auth.accessToken, - 'client': auth.client + + for (let i = 0; i < maxRetries; i++) { + // delay + await new Promise(resolve => setTimeout(resolve, interval)) + + const response = await rp({ + uri, + qs, + headers, + simple: false, + resolveWithFullResponse: true + }).catch(error => { + logger.error('Error downloading from INK:', error.message) + throw error + }) + + // a successful request: return the data + if (response.statusCode === 200) { + return response.body + } + + // not a 404 response - stop trying + if (response.statusCode !== 404) { + break } } - return promiseRetry( - (retry, number) => { - return rp(downloadRequest).catch(retry) - }, - { retries: maxRetries, factor: 1, minTimeout: 3000 } - ) + throw new Error('Unable to download the output from INK') } -const downloadUrl = (chainId, relPath) => inkEndpoint + - '/api/process_chains/' + - chainId + - '/download_output_file?relative_path=' + - relPath + - '.html' +const findRecipeId = (recipeKey = 'Editoria Typescript', auth) => rp({ + method: 'GET', + uri: inkUrl('recipes'), + headers: { + uid: inkConfig.email, + ...auth + }, + json: true +}).then(data => { + const recipe = data.recipes.find(recipe => recipe.name === recipeKey) -const downloadFromInk = ([auth, response]) => { - if (response.process_chain.input_file_manifest.length === 0) { - throw new Error('The INK server gave a malformed response (no input files in the process chain)') - } - const relPath = path.basename(response.process_chain.input_file_manifest[0].path, '.docx') - const url = downloadUrl(response.process_chain.id, relPath) - return retryFor30SecondsUntil200(url, auth) + return recipe ? recipe.id : null +}) + +const process = async (inputFile, options) => { + const auth = await authorize().catch(err => { + logger.error('INK API LOGIN FAILURE:', err.message) + throw err + }) + + // either use the recipe id from the configuration or search for it by name + const recipeId = inkConfig.recipes[options.recipe] || await findRecipeId(options.recipe, auth) + if (!recipeId) throw new Error('Unknown recipe') + + const response = await upload(recipeId, inputFile, auth).catch(err => { + logger.error('INK API UPLOAD FAILURE:', err.message) + throw err + }) + + return download(response.process_chain, auth, options.outputFileName) } -var InkBackend = function (app) { +const InkBackend = function (app) { + // TODO: authentication on this route app.use('/api/ink', (req, res, next) => { - var fileStream = new Busboy({ headers: req.headers }) - - const handleErr = err => { - logger.error('ERROR CONVERTING WITH INK', err) - next(err) - } + const fileStream = new Busboy({ headers: req.headers }) fileStream.on('file', (fieldname, file, filename, encoding, contentType) => { - var stream = temp.createWriteStream() - file.pipe(stream) - - file.on('end', () => { - stream.end() + const stream = temp.createWriteStream() - var fileOpts = { + stream.on('finish', () => { + const inputFile = { value: fs.createReadStream(stream.path), - options: { - filename: filename, - contentType: contentType - } + options: { filename, contentType } } - getAuth().then( - checkInk - ).then( - uploadToInk(fileOpts) - ).then( - downloadFromInk - ).then( - response => res.send(response) - ).catch(handleErr) + process(inputFile, req.query).then(converted => { + res.json({ converted }) + + // clean up temp file + fs.unlink(stream.path, () => { + logger.info('Deleted temporary file', stream.path) + }) + }).catch(err => { + logger.error('ERROR CONVERTING WITH INK:', err.message) + next(err) + }) + }) + + file.pipe(stream) + + file.on('end', () => { + stream.end() }) }) - fileStream.on('error', handleErr) + fileStream.on('error', err => { + logger.error(err) + next(err) + }) req.pipe(fileStream) }) diff --git a/packages/InkBackend/package.json b/packages/InkBackend/package.json index 94229aa9e4bea5d851f28f15d038fad067d2d9c2..80e7fd3b72d29486e0e8e2436447fc24d0503496 100644 --- a/packages/InkBackend/package.json +++ b/packages/InkBackend/package.json @@ -15,9 +15,8 @@ "@pubsweet/logger": "^0.0.1", "busboy": "^0.2.13", "config": "^1.26.1", - "promise-retry": "^1.1.1", - "request": "^2.79.0", - "request-promise-native": "^1.0.3", + "request": "^2.83.0", + "request-promise-native": "^1.0.5", "temp": "^0.8.3" } } diff --git a/packages/InkBackend/yarn.lock b/packages/InkBackend/yarn.lock index 489024b94451f2ac8fd61b73ace657eee05f4c82..e6cbed36c05b4084eb0c76253c238eed23b7619d 100644 --- a/packages/InkBackend/yarn.lock +++ b/packages/InkBackend/yarn.lock @@ -9,12 +9,14 @@ config "^1.26.2" joi "^10.6.0" -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" +ajv@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9" dependencies: co "^4.6.0" - json-stable-stringify "^1.0.1" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" asn1@~0.2.3: version "0.2.3" @@ -24,19 +26,15 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws4@^1.2.1: +aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -46,11 +44,17 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" dependencies: - hoek "2.x.x" + hoek "4.x.x" busboy@^0.2.13: version "0.2.14" @@ -74,21 +78,21 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: delayed-stream "~1.0.0" config@^1.26.1, config@^1.26.2: - version "1.26.2" - resolved "https://registry.yarnpkg.com/config/-/config-1.26.2.tgz#2466291168d8afae0aae8ab99ea4d4272f520cae" + version "1.28.1" + resolved "https://registry.yarnpkg.com/config/-/config-1.28.1.tgz#7625d2a1e4c90f131d8a73347982d93c3873282d" dependencies: json5 "0.4.0" os-homedir "1.0.2" -core-util-is@~1.0.0: +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" dependencies: - boom "2.x.x" + boom "5.x.x" dashdash@^1.12.0: version "1.14.1" @@ -113,25 +117,29 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - -extend@~3.0.0: +extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" dependencies: asynckit "^0.4.0" combined-stream "^1.0.5" @@ -143,39 +151,35 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" + ajv "^5.1.0" + har-schema "^2.0.0" -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" dependencies: - assert-plus "^0.2.0" + assert-plus "^1.0.0" jsprim "^1.2.2" sshpk "^1.7.0" @@ -216,16 +220,14 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -234,34 +236,30 @@ json5@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" dependencies: assert-plus "1.0.0" - extsprintf "1.0.2" + extsprintf "1.3.0" json-schema "0.2.3" - verror "1.3.6" + verror "1.10.0" lodash@^4.13.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" +mime-types@^2.1.12, mime-types@~2.1.17: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: - mime-db "~1.27.0" + mime-db "~1.30.0" -oauth-sign@~0.8.1: +oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -273,24 +271,17 @@ os-tmpdir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - dependencies: - err-code "^1.0.0" - retry "^0.10.0" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" readable-stream@1.1.x: version "1.1.14" @@ -307,58 +298,54 @@ request-promise-core@1.1.1: dependencies: lodash "^4.13.1" -request-promise-native@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.4.tgz#86988ec8eee408e45579fce83bfd05b3adf9a155" +request-promise-native@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" dependencies: request-promise-core "1.1.1" stealthy-require "^1.1.0" - tough-cookie ">=2.3.0" + tough-cookie ">=2.3.3" -request@^2.79.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" +request@^2.83.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" + aws-sign2 "~0.7.0" + aws4 "^1.6.0" caseless "~0.12.0" combined-stream "~1.0.5" - extend "~3.0.0" + extend "~3.0.1" forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" tunnel-agent "^0.6.0" - uuid "^3.0.0" - -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + uuid "^3.1.0" rimraf@~2.2.6: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" -safe-buffer@^5.0.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" dependencies: - hoek "2.x.x" + hoek "4.x.x" sshpk@^1.7.0: version "1.13.1" @@ -386,7 +373,7 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -stringstream@~0.0.4: +stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -403,9 +390,9 @@ topo@2.x.x: dependencies: hoek "4.x.x" -tough-cookie@>=2.3.0, tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" +tough-cookie@>=2.3.3, tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" @@ -419,12 +406,14 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" -uuid@^3.0.0: +uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" dependencies: - extsprintf "1.0.2" + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" diff --git a/packages/InkFrontend/InkFrontend.jsx b/packages/InkFrontend/InkFrontend.jsx index 78e7282be75e4125c80ef0c68fe3b136612f8f48..e18d40133a1cdac59c2817d51daa8ddb27682a92 100644 --- a/packages/InkFrontend/InkFrontend.jsx +++ b/packages/InkFrontend/InkFrontend.jsx @@ -3,41 +3,38 @@ import PropTypes from 'prop-types' import { Alert, Row, Col } from 'react-bootstrap' import Dropzone from 'react-dropzone' -import escapeHtml from 'escape-html' - export default class InkFrontend extends Component { constructor (props) { super(props) this.onDrop = this.onDrop.bind(this) } + onDrop (files) { + this.props.convert(files[0]) + } + render () { - const { ink, error } = this.props + const { ink } = this.props return ( <div className='bootstrap'> { ink.isFetching ? <Alert bsStyle="info">INK is doing its thing...</Alert> : null } - { error ? <Alert bsStyle="danger">INK failed</Alert> : null } + { ink.error ? <Alert bsStyle="danger">INK failed</Alert> : null } <Row> <Col xs={12} md={2} mdOffset={5}> <Dropzone onDrop={this.onDrop} multiple={false}> <div>Try dropping some files here, or click to select files to upload.</div> </Dropzone> - { ink.converted ? <span>HERE { escapeHtml(ink.converted)}</span> : null} + { ink.converted ? <span>HERE { ink.converted }</span> : null} </Col> </Row> </div> ) } - - onDrop (files) { - this.props.actions.ink(files[0]) - } } InkFrontend.propTypes = { - actions: PropTypes.object, - error: PropTypes.string, + convert: PropTypes.func, ink: PropTypes.object } diff --git a/packages/InkFrontend/InkFrontendContainer.js b/packages/InkFrontend/InkFrontendContainer.js index 77fa450973b13bce3d782de9e748c9de59d3a3ca..86e409c17a23638e0f197594d4c5573d589c79bf 100644 --- a/packages/InkFrontend/InkFrontendContainer.js +++ b/packages/InkFrontend/InkFrontendContainer.js @@ -1,22 +1,12 @@ -import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { ink } from './actions' - +import { ink as convert } from './actions' import InkFrontend from './InkFrontend' -function mapState (state) { - return { - error: state.error, +export default connect( + state => ({ ink: state.ink + }), + { + convert } -} - -function mapDispatch (dispatch) { - return { - actions: bindActionCreators({ ink }, dispatch) - } -} - -export default connect( - mapState, mapDispatch )(InkFrontend) diff --git a/packages/InkFrontend/actions.js b/packages/InkFrontend/actions.js index eed368e3209a5bc871ee437d10e0e4c7ec82fa08..e5a014aa35c14937bb3a12f4fa7322e950c24e34 100644 --- a/packages/InkFrontend/actions.js +++ b/packages/InkFrontend/actions.js @@ -1,51 +1,43 @@ +import qs from 'query-string' import request from 'pubsweet-client/src/helpers/api' -import * as T from './types' -import config from 'config' +import { INK_FAILURE, INK_REQUEST, INK_SUCCESS } from './types' -const ENDPOINT = config['pubsweet-client']['API_ENDPOINT'].replace(/api$/, 'ink') +export const inkRequest = () => ({ + type: INK_REQUEST +}) -function inkRequest () { - return { - type: T.INK_REQUEST, - isFetching: true - } -} +export const inkSuccess = converted => ({ + type: INK_SUCCESS, + converted +}) -function inkSuccess (converted) { - return { - type: T.INK_SUCCESS, - isFetching: false, - converted: converted - } -} +export const inkFailure = error => ({ + type: INK_FAILURE, + error +}) -function inkFailure (message) { - return { - type: T.INK_FAILURE, - isFetching: false, - error: message - } -} +export const ink = (file, options) => dispatch => { + dispatch(inkRequest()) -// Calls the API to get a token and -// dispatches actions along the way -export function ink (file) { - const data = new FormData() - data.append('file', file) + const body = new FormData() + body.append('file', file) - const options = { - method: 'POST', - body: data, - parse: false - } + let url = '/ink' - return dispatch => { - dispatch(inkRequest()) - return request(ENDPOINT, options) - .then( - response => response.text() // TODO: return JSON from the backend - ).then( - response => dispatch(inkSuccess(response)) - ).catch(err => dispatch(inkFailure(err))) + if (options) { + url += '?' + qs.stringify(options) } + + return request(url, { + method: 'POST', + body + }).then(data => { + dispatch(inkSuccess(data.converted)) + + return data + }).catch(error => { + dispatch(inkFailure(error.message)) + + throw error + }) } diff --git a/packages/InkFrontend/package.json b/packages/InkFrontend/package.json index 4a1c54406a862dbfd6b8ca82824e5fae3e437979..6ede87d3d61d3771187649c1e8ec869369cd50e7 100644 --- a/packages/InkFrontend/package.json +++ b/packages/InkFrontend/package.json @@ -21,8 +21,8 @@ "author": "Collaborative Knowledge Foundation", "license": "MIT", "dependencies": { - "escape-html": "^1.0.3", "prop-types": "^15.5.10", + "query-string": "^5.0.1", "react-bootstrap": "^0.31.3", "react-dropzone": "^3.7.3", "react-redux": "^5.0.6", diff --git a/packages/InkFrontend/reducers.js b/packages/InkFrontend/reducers.js index a1928a81a2abfd0a757ac6377715804d259319e3..97b8298525bce7130f9782c7fe2821db5f2496bf 100644 --- a/packages/InkFrontend/reducers.js +++ b/packages/InkFrontend/reducers.js @@ -1,25 +1,34 @@ -import { - INK_REQUEST, INK_SUCCESS, INK_FAILURE -} from './types' +import { INK_FAILURE, INK_REQUEST, INK_SUCCESS } from './types' -export default function ink (state = { - isFetching: false -}, action) { +const initialState = { + isFetching: false, + converted: null, + error: null +} + +export default function ink (state = initialState, action) { switch (action.type) { case INK_REQUEST: - return Object.assign({}, state, { - isFetching: true - }) + return { + isFetching: true, + converted: null, + error: null + } + case INK_SUCCESS: - return Object.assign({}, state, { + return { isFetching: false, - converted: action.converted - }) + converted: action.converted, + error: null + } + case INK_FAILURE: - return Object.assign({}, state, { + return { isFetching: false, + converted: null, error: action.error - }) + } + default: return state } diff --git a/packages/InkFrontend/types.js b/packages/InkFrontend/types.js index ec7050e72e7ee2e4129c8458b3c61b7618906791..a521cd964b6f0e42cd62e549e4fac376abe772ae 100644 --- a/packages/InkFrontend/types.js +++ b/packages/InkFrontend/types.js @@ -1,5 +1,3 @@ -// Action types - export const INK_REQUEST = 'INK_REQUEST' export const INK_SUCCESS = 'INK_SUCCESS' export const INK_FAILURE = 'INK_FAILURE' diff --git a/packages/InkFrontend/yarn.lock b/packages/InkFrontend/yarn.lock index 0c5e4b428d7e36c956354e3b024347bc8e324851..197f73df48725d29efb6fca401e2c2c5574df4ac 100644 --- a/packages/InkFrontend/yarn.lock +++ b/packages/InkFrontend/yarn.lock @@ -3,8 +3,8 @@ asap@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" attr-accept@^1.0.3: version "1.1.0" @@ -29,7 +29,11 @@ core-js@^2.4.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" -dom-helpers@^3.2.0: +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +dom-helpers@^3.2.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" @@ -39,13 +43,9 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -escape-html@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -fbjs@^0.8.9: - version "0.8.12" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: core-js "^1.0.0" isomorphic-fetch "^2.1.1" @@ -60,8 +60,8 @@ hoist-non-react-statics@^2.2.1: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" iconv-lite@~0.4.13: - version "0.4.18" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1: version "2.2.2" @@ -103,13 +103,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: js-tokens "^3.0.0" node-fetch@^1.0.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5" + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" dependencies: encoding "^0.1.11" is-stream "^1.0.1" -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -125,16 +125,25 @@ prop-types-extra@^1.0.1: dependencies: warning "^3.0.0" -prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8: - version "15.5.10" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" +prop-types@^15.5.10, prop-types@^15.5.7: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: - fbjs "^0.8.9" + fbjs "^0.8.16" loose-envify "^1.3.1" + object-assign "^4.1.1" + +query-string@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" react-bootstrap@^0.31.3: - version "0.31.3" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.3.tgz#db2b7d45b00b5dac1ab8b6de3dd97feb3091b849" + version "0.31.5" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.5.tgz#57040fa8b1274e1e074803c21a1b895fdabea05a" dependencies: babel-runtime "^6.11.6" classnames "^2.2.5" @@ -143,32 +152,25 @@ react-bootstrap@^0.31.3: keycode "^2.1.2" prop-types "^15.5.10" prop-types-extra "^1.0.1" - react-overlays "^0.7.0" - react-prop-types "^0.4.0" + react-overlays "^0.7.4" uncontrollable "^4.1.0" warning "^3.0.0" react-dropzone@^3.7.3: - version "3.13.3" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-3.13.3.tgz#b8bde4b5a12842f85196b45e8cc2959834b81962" + version "3.13.4" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-3.13.4.tgz#84da26815c40339691c49b4544c2ef7a16912ccc" dependencies: attr-accept "^1.0.3" prop-types "^15.5.7" -react-overlays@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.7.0.tgz#531898ff566c7e5c7226ead2863b8cf9fbb5a981" +react-overlays@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.7.4.tgz#ef2ec652c3444ab8aa014262b18f662068e56d5c" dependencies: classnames "^2.2.5" - dom-helpers "^3.2.0" - prop-types "^15.5.8" - react-prop-types "^0.4.0" - warning "^3.0.0" - -react-prop-types@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0" - dependencies: + dom-helpers "^3.2.1" + prop-types "^15.5.10" + prop-types-extra "^1.0.1" warning "^3.0.0" react-redux@^5.0.6: @@ -199,13 +201,17 @@ setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + symbol-observable@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + version "1.1.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" ua-parser-js@^0.7.9: - version "0.7.13" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.13.tgz#cd9dd2f86493b3f44dbeeef3780fda74c5ee14be" + version "0.7.17" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" uncontrollable@^4.1.0: version "4.1.0"