backend/app/controllers/api/v1/Export.controller.js
garrettmills c86b82bd0c
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
fix escaping regex in HTML export (#13)
2020-10-19 09:13:24 -05:00

274 lines
9.9 KiB
JavaScript

const { Controller } = require('libflitter')
const ncp = require('ncp').ncp
const fs = require('fs').promises
const path = require('path')
const md = require('markdown').markdown
const rimraf = require('rimraf')
const uuid = require('uuid/v4')
class ExportController extends Controller {
static get services() {
return [...super.services, 'models', 'utility', 'upload']
}
async get_export_list(req, res, next) {
const Export = this.models.get('api:Export')
const exports = await Export.find({
user_id: req.user.id,
})
return res.api(exports)
}
async download_export(req, res, next) {
const Export = this.models.get('api:Export')
const exported = await Export.findOne({ UUID: req.params.ExportId, user_id: req.user.id })
if ( !exported ) {
return res.status(404)
.message('No export with that UUID found.')
.api()
}
const File = this.models.get('upload::File')
const file = await File.findOne({_id: File.ObjectId(exported.file_id)})
if ( !file ) return res.status(404).message('This export file has been deleted.').api({})
return file.send(res)
}
async export_subtree(req, res, next) {
const Export = this.models.get('api:Export')
const format = req.form.format
const page = req.form.page
if ( format === 'html' ) {
const generated_export = await this.export_subtree_as_html(page, req.user)
// Store the generated archive
const uploader = this.upload.provider()
const file = await uploader.store({
temp_path: generated_export,
original_name: `export-${uuid()}.tar.gz`,
mime_type: 'application/gzip',
tag: 'generated_export',
})
const exp = new Export({
user_id: req.user.id,
format,
subtree: true,
file_id: file.id,
PageId: page.UUID,
})
await exp.save()
return res.api(exp)
}
return res.status(400)
.message('Invalid export format!')
.api()
}
async export_subtree_as_html(page, user) {
const flat_tree = []
const add_to_tree = async (page, level = 0) => {
if ( await page.is_accessible_by(user, 'view') ) {
flat_tree.push({
level,
page,
file_name: `${page.Name.replace(/\s/g, '-').replace(/[\/\\]/g, '-')}-${page.UUID}.html`,
})
const children = await page.childPages
for ( const child of children ) {
await add_to_tree(child, level + 1)
}
}
}
await add_to_tree(page)
const manifest = {
sidebar: [],
}
for ( const item of flat_tree ) {
manifest.sidebar.push({
title: item.page.Name,
level: item.level,
link: item.file_name,
})
}
// Copy the template over
const work_dir = await this.scratch_dir()
await this.copy_template(this.utility.path('app', 'assets', 'export', 'html'), work_dir)
const html_template = await fs.readFile(path.resolve(work_dir, 'index.html'), 'utf-8')
for ( const item of flat_tree ) {
let item_template = html_template
item_template = item_template.replace(/{{\s?MANIFEST\s?}}/g, JSON.stringify(manifest))
item_template = item_template.replace(/{{\s?GROUP_TITLE\s?}}/g, page.Name)
item_template = item_template.replace(/{{\s?PAGE_TITLE\s?}}/g, item.page.Name)
item_template = item_template.replace(/{{\s?PAGE_CONTENT\s?}}/g, await this.page_as_html(item.page, work_dir))
await fs.writeFile(path.resolve(work_dir, item.file_name), item_template)
}
// Write the main page redirect
const redir_html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=${flat_tree[0].file_name}">
</head>
</html>
`
await fs.writeFile(path.resolve(work_dir, 'index.html'), redir_html)
// Create the archive
const tar = require('tar')
const archive_path = path.resolve(work_dir, '..', `export-${uuid()}.tar.gz`)
await tar.c({
gzip: true,
file: archive_path,
cwd: path.resolve(work_dir, '..'),
}, [path.basename(work_dir)])
await new Promise(res => {
rimraf(work_dir, res)
})
return archive_path
}
async copy_template(from, to) {
return new Promise((res, rej) => {
ncp(from, to, err => {
if ( err ) rej(err)
else res()
})
})
}
async scratch_dir() {
const tmp = require('tmp')
const gen_id = uuid()
return new Promise((res, rej) => {
tmp.dir((err, tmp_path) => {
if ( err ) rej(err)
else {
fs.mkdir(path.resolve(tmp_path, `export`)).then(() => {
res(path.resolve(tmp_path, `export`))
})
}
})
})
}
/**
* Given a page and a template working directory, render that page as HTML.
* @param {Page} page
* @param {string} work_dir
* @return {Promise<string>}
*/
async page_as_html(page, work_dir) {
const Codium = this.models.get('api:Codium')
const FileGroup = this.models.get('api:FileGroup')
const File = this.models.get('upload::File')
const Database = this.models.get('api:db:Database')
const DBEntry = this.models.get('api:db:DBEntry')
let html = ''
const nodes = await page.nodes
for ( const node of nodes ) {
// ATM, there are 5 node types: norm, markdown, database_ref, files_ref, and code_ref
if ( node.Value.Mode === 'norm' ) {
html += node.Value.Value
} else if ( node.Value.Mode === 'markdown' ) {
html += md.toHTML(node.Value.Value)
} else if ( node.Type === 'code_ref' ) {
const code = await Codium.findOne({ UUID: node.Value.Value })
if ( code ) {
const snip_file = `code-snippet-${code.UUID}.txt`
await fs.writeFile(path.resolve(work_dir, snip_file), code.code)
html += `
<div class="code-ref">
<wc-monaco-editor id="${code.UUID}" language="${code.Language}" src="${snip_file}"></wc-monaco-editor>
</div>
`
}
} else if ( node.Type === 'file_ref' ) {
const file_group = await FileGroup.findOne({ UUID: node.Value.Value })
if ( file_group ) {
const file_htmls = []
for ( const file_id of file_group.FileIds ) {
const file = await File.findById(file_id)
if ( file ) {
const store_path = file.provider().filepath(file.store_id)
const ext = file.original_name.split('.').reverse()[0]
await this.copy_template(store_path, path.resolve(work_dir, `file-${file.upload_name}.${ext}`))
file_htmls.push(`
<div class="file">
<div class="file-name">${file.original_name}</div>
<a href="file-${file.upload_name}.${ext}" class="dl-link" target="_blank">▼</a>
</div>
`)
}
}
html += `
<div class="file-ref">
${file_htmls.join('\n')}
</div>`
}
} else if ( node.Type === 'database_ref' ) {
const grid_id = `dbase-${node.Value.Value}`
const db = await Database.findOne({ UUID: node.Value.Value })
if ( db ) {
html += `
<div id="${grid_id}-container" class="database-container">
<h4>${db.Name}</h4>
<div id="${grid_id}" class="ag-theme-balham database-ref"></div>
</div>
<script src="${grid_id}.js"></script>
`
// generate the column defs
const cols = await db.get_columns()
const col_defs = cols.map(col => {
return {
field: col.field,
headerName: col.headerName,
}
})
// generate the data rows
const entries = await DBEntry.find({ DatabaseId: db.UUID })
const rows = entries.map(entry => entry.RowData)
let dbTemplateContents = await fs.readFile(path.resolve(work_dir, 'database.js'), 'utf-8')
dbTemplateContents = dbTemplateContents.replace(/{{\s?COLUMN_DEFS\s?}}/g, JSON.stringify(col_defs))
dbTemplateContents = dbTemplateContents.replace(/{{\s?ROW_DATA\s?}}/g, JSON.stringify(rows))
dbTemplateContents = dbTemplateContents.replace(/{{\s?GRID_ID\s?}}/g, grid_id)
await fs.writeFile(path.resolve(work_dir, `${grid_id}.js`), dbTemplateContents)
}
}
}
return html
}
}
module.exports = exports = ExportController