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, '-')}-${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 = ` ` 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} */ 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 += `
` } } 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(`
${file.original_name}
`) } } html += `
${file_htmls.join('\n')}
` } } 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 += `

${db.Name}

` // 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