Skip to content
Snippets Groups Projects

Allow different recipes to be called via the INK component

Merged Alf Eaton requested to merge ink-backend-recipe-parameter into master
10 files
+ 372
434
Compare changes
  • Side-by-side
  • Inline
Files
10
const config = require('config')
const rp = require('request-promise-native')
const Busboy = require('busboy')
const temp = require('temp').track()
const fs = require('fs')
const fs = require('fs')
const path = require('path')
const path = require('path')
const promiseRetry = require('promise-retry')
const logger = require('@pubsweet/logger')
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')
// Generate the absolute URL
let inkEndpoint = inkConfig.inkEndpoint
const inkUrl = path => inkConfig.inkEndpoint + 'api/' + path
let email = inkConfig.email
let password = inkConfig.password
let maxRetries = inkConfig.maxRetries || 60
let authRequest = {
// Sign in
uri: inkEndpoint + '/api/auth/sign_in',
const authorize = () => rp({
method: 'POST',
method: 'POST',
body: {
uri: inkUrl('auth/sign_in'),
email: email,
formData: {
password: password
email: inkConfig.email,
 
password: inkConfig.password
},
},
json: true,
headers: {
headers: {
'Accept': 'application/vnd.ink.1'
'Accept': 'application/vnd.ink.1'
},
},
resolveWithFullResponse: true
resolveWithFullResponse: true
}
}).then(res => ({
 
'client': res.headers['client'],
 
'access-token': res.headers['access-token']
 
}))
// Get an access token
// Upload file to INK and execute the recipe
let getAuth = () => {
const upload = (recipeId, inputFile, auth) => rp({
return rp(authRequest).then(response => {
method: 'POST',
return {
uri: inkUrl('recipes/' + recipeId + '/execute'),
accessToken: response.headers['access-token'],
headers: {
client: response.headers['client']
uid: inkConfig.email,
}
...auth
}).catch(
},
err => {
formData: {
logger.error('INK API LOGIN FAILURE:', err)
input_files: [inputFile]
throw err
},
}
json: true,
)
timeout: 60 * 60 * 1000 // 3600 seconds
}
})
let defaultRecipeId = null
// Download the output file (keep trying if there's a 404 response, until it's ready)
let recipeListUrl = inkEndpoint + '/api/recipes'
const download = async (chain, auth, outputFileName) => {
 
const manifest = chain.input_file_manifest
const getRecipeId = () => {
if (manifest.length === 0) {
if (defaultRecipeId) return Promise.resolve(defaultRecipeId)
throw new Error('The INK server gave a malformed response (no input files in the process chain)')
 
}
const listRequest = auth => ({
const interval = inkConfig.interval || 1000 // try once per second
method: 'GET',
uri: recipeListUrl,
headers: {
'uid': email,
'access-token': auth.accessToken,
'client': auth.client
}
})
return getAuth().then(
const maxRetries = inkConfig.maxRetries || 300 // retry for up to 5 minutes
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)
}
)
}
getRecipeId()
const uri = inkUrl('process_chains/' + chain.id + '/download_output_file')
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)
}
)
// Upload file to INK and execute the recipe
const qs = {
const uploadToInk = data => auth => uploadRequest(
relative_path: outputFileName || path.basename(manifest[0].path, '.docx') + '.html'
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)
}
}
).catch(
err => {
const headers = {
throw err
uid: inkConfig.email,
 
...auth
}
}
)
for (let i = 0; i < maxRetries; i++) {
const retryFor30SecondsUntil200 = (uri, auth) => {
// delay
const downloadRequest = {
await new Promise(resolve => setTimeout(resolve, interval))
method: 'GET',
uri: uri,
const response = await rp({
headers: {
uri,
'uid': email,
qs,
'access-token': auth.accessToken,
headers,
'client': auth.client
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(
throw new Error('Unable to download the output from INK')
(retry, number) => {
return rp(downloadRequest).catch(retry)
},
{ retries: maxRetries, factor: 1, minTimeout: 3000 }
)
}
}
const downloadUrl = (chainId, relPath) => inkEndpoint +
const findRecipeId = (recipeKey = 'Editoria Typescript', auth) => rp({
'/api/process_chains/' +
method: 'GET',
chainId +
uri: inkUrl('recipes'),
'/download_output_file?relative_path=' +
headers: {
relPath +
uid: inkConfig.email,
'.html'
...auth
 
},
 
json: true
 
}).then(data => {
 
const recipe = data.recipes.find(recipe => recipe.name === recipeKey)
const downloadFromInk = ([auth, response]) => {
return recipe ? recipe.id : null
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 process = async (inputFile, options) => {
const relPath = path.basename(response.process_chain.input_file_manifest[0].path, '.docx')
const auth = await authorize().catch(err => {
const url = downloadUrl(response.process_chain.id, relPath)
logger.error('INK API LOGIN FAILURE:', err.message)
return retryFor30SecondsUntil200(url, auth)
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) => {
app.use('/api/ink', (req, res, next) => {
var fileStream = new Busboy({ headers: req.headers })
const fileStream = new Busboy({ headers: req.headers })
const handleErr = err => {
logger.error('ERROR CONVERTING WITH INK', err)
next(err)
}
fileStream.on('file', (fieldname, file, filename, encoding, contentType) => {
fileStream.on('file', (fieldname, file, filename, encoding, contentType) => {
var stream = temp.createWriteStream()
const stream = temp.createWriteStream()
file.pipe(stream)
file.on('end', () => {
stream.end()
var fileOpts = {
stream.on('finish', () => {
 
const inputFile = {
value: fs.createReadStream(stream.path),
value: fs.createReadStream(stream.path),
options: {
options: { filename, contentType }
filename: filename,
contentType: contentType
}
}
}
getAuth().then(
process(inputFile, req.query).then(converted => {
checkInk
res.json({ converted })
).then(
uploadToInk(fileOpts)
// clean up temp file
).then(
fs.unlink(stream.path, () => {
downloadFromInk
logger.info('Deleted temporary file', stream.path)
).then(
})
response => res.send(response)
}).catch(err => {
).catch(handleErr)
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)
req.pipe(fileStream)
})
})