Merge pull request #3042 from Budibase/fix/app-export-performance

Improve app export experience
This commit is contained in:
Andrew Kingston 2021-10-20 10:25:59 +01:00 committed by GitHub
commit 029b447ce7
5 changed files with 76 additions and 61 deletions

View File

@ -41,6 +41,7 @@ static_resources:
- match: { prefix: "/api/" } - match: { prefix: "/api/" }
route: route:
cluster: server-dev cluster: server-dev
timeout: 120s
- match: { prefix: "/app_" } - match: { prefix: "/app_" }
route: route:

View File

@ -58,6 +58,7 @@ static_resources:
- match: { prefix: "/api/" } - match: { prefix: "/api/" }
route: route:
cluster: app-service cluster: app-service
timeout: 120s
- match: { prefix: "/worker/" } - match: { prefix: "/worker/" }
route: route:

View File

@ -112,16 +112,8 @@
const exportApp = app => { const exportApp = app => {
const id = app.deployed ? app.prodId : app.devId const id = app.deployed ? app.prodId : app.devId
try { const appName = encodeURIComponent(app.name)
download( window.location = `/api/backups/export?appId=${id}&appname=${appName}`
`/api/backups/export?appId=${id}&appname=${encodeURIComponent(
app.name
)}`
)
notifications.success("App exported successfully")
} catch (err) {
notifications.error(`Error exporting app: ${err}`)
}
} }
const unpublishApp = app => { const unpublishApp = app => {

View File

@ -1,10 +1,9 @@
const { performBackup } = require("../../utilities/fileSystem") const { streamBackup } = require("../../utilities/fileSystem")
exports.exportAppDump = async function (ctx) { exports.exportAppDump = async function (ctx) {
const { appId } = ctx.query const { appId } = ctx.query
const appname = decodeURI(ctx.query.appname) const appName = decodeURI(ctx.query.appname)
const backupIdentifier = `${appname}Backup${new Date().getTime()}.txt` const backupIdentifier = `${appName}-export-${new Date().getTime()}.txt`
ctx.attachment(backupIdentifier) ctx.attachment(backupIdentifier)
ctx.body = await performBackup(appId, backupIdentifier) ctx.body = await streamBackup(appId)
} }

View File

@ -106,56 +106,67 @@ exports.apiFileReturn = contents => {
} }
/** /**
* Takes a copy of the database state for an app to the object store. * Local utility to back up the database state for an app, excluding global user
* @param {string} appId The ID of the app which is to be backed up. * data or user relationships.
* @param {string} backupName The name of the backup located in the object store. * @param {string} appId The app to backup
* @return The backup has been completed when this promise completes and returns a file stream * @param {object} config Config to send to export DB
* to the temporary backup file (to return via API if required). * @returns {*} either a string or a stream of the backup
*/ */
exports.performBackup = async (appId, backupName) => { const backupAppData = async (appId, config) => {
return exports.exportDB(appId, { return await exports.exportDB(appId, {
exportName: backupName, ...config,
filter: doc => filter: doc =>
!( !(
doc._id.includes(USER_METDATA_PREFIX) || doc._id.includes(USER_METDATA_PREFIX) ||
doc.includes(LINK_USER_METADATA_PREFIX) doc._id.includes(LINK_USER_METADATA_PREFIX)
), ),
}) })
} }
/** /**
* exports a DB to either file or a variable (memory). * Takes a copy of the database state for an app to the object store.
* @param {string} dbName the DB which is to be exported. * @param {string} appId The ID of the app which is to be backed up.
* @param {string} exportName optional - the file name to export to, if not in memory. * @param {string} backupName The name of the backup located in the object store.
* @param {function} filter optional - a filter function to clear out any un-wanted docs. * @return {*} a readable stream to the completed backup file
* @return Either the file stream or the variable (if no export name provided).
*/ */
exports.exportDB = async ( exports.performBackup = async (appId, backupName) => {
dbName, return await backupAppData(appId, { exportName: backupName })
{ exportName, filter } = { exportName: undefined, filter: undefined }
) => {
let stream,
appString = "",
path = null
if (exportName) {
path = join(budibaseTempDir(), exportName)
stream = fs.createWriteStream(path)
} else {
stream = new MemoryStream()
stream.on("data", chunk => {
appString += chunk.toString()
})
} }
// perform couch dump
/**
* Streams a backup of the database state for an app
* @param {string} appId The ID of the app which is to be backed up.
* @returns {*} a readable stream of the backup which is written in real time
*/
exports.streamBackup = async appId => {
return await backupAppData(appId, { stream: true })
}
/**
* Exports a DB to either file or a variable (memory).
* @param {string} dbName the DB which is to be exported.
* @param {string} exportName optional - provide a filename to write the backup to a file
* @param {boolean} stream optional - whether to perform a full backup
* @param {function} filter optional - a filter function to clear out any un-wanted docs.
* @return {*} either a readable stream or a string
*/
exports.exportDB = async (dbName, { stream, filter, exportName } = {}) => {
const instanceDb = new CouchDB(dbName) const instanceDb = new CouchDB(dbName)
await instanceDb.dump(stream, {
filter, // Stream the dump if required
}) if (stream) {
// just in memory, return the final string const memStream = new MemoryStream()
if (!exportName) { instanceDb.dump(memStream, { filter })
return appString return memStream
} }
// write the file to the object store
// Write the dump to file if required
if (exportName) {
const path = join(budibaseTempDir(), exportName)
const writeStream = fs.createWriteStream(path)
await instanceDb.dump(writeStream, { filter })
// Upload the dump to the object store if self hosted
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
await streamUpload( await streamUpload(
ObjectStoreBuckets.BACKUPS, ObjectStoreBuckets.BACKUPS,
@ -163,9 +174,20 @@ exports.exportDB = async (
fs.createReadStream(path) fs.createReadStream(path)
) )
} }
return fs.createReadStream(path) return fs.createReadStream(path)
} }
// Stringify the dump in memory if required
const memStream = new MemoryStream()
let appString = ""
memStream.on("data", chunk => {
appString += chunk.toString()
})
await instanceDb.dump(memStream, { filter })
return appString
}
/** /**
* Writes the provided contents to a temporary file, which can be used briefly. * Writes the provided contents to a temporary file, which can be used briefly.
* @param {string} fileContents contents which will be written to a temp file. * @param {string} fileContents contents which will be written to a temp file.