Merge pull request #3042 from Budibase/fix/app-export-performance
Improve app export experience
This commit is contained in:
commit
029b447ce7
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,64 +106,86 @@ 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 = "",
|
* Streams a backup of the database state for an app
|
||||||
path = null
|
* @param {string} appId The ID of the app which is to be backed up.
|
||||||
if (exportName) {
|
* @returns {*} a readable stream of the backup which is written in real time
|
||||||
path = join(budibaseTempDir(), exportName)
|
*/
|
||||||
stream = fs.createWriteStream(path)
|
exports.streamBackup = async appId => {
|
||||||
} else {
|
return await backupAppData(appId, { stream: true })
|
||||||
stream = new MemoryStream()
|
}
|
||||||
stream.on("data", chunk => {
|
|
||||||
appString += chunk.toString()
|
/**
|
||||||
})
|
* Exports a DB to either file or a variable (memory).
|
||||||
}
|
* @param {string} dbName the DB which is to be exported.
|
||||||
// perform couch dump
|
* @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) {
|
||||||
|
const memStream = new MemoryStream()
|
||||||
|
instanceDb.dump(memStream, { filter })
|
||||||
|
return memStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
await streamUpload(
|
||||||
|
ObjectStoreBuckets.BACKUPS,
|
||||||
|
join(dbName, exportName),
|
||||||
|
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()
|
||||||
})
|
})
|
||||||
// just in memory, return the final string
|
await instanceDb.dump(memStream, { filter })
|
||||||
if (!exportName) {
|
return appString
|
||||||
return appString
|
|
||||||
}
|
|
||||||
// write the file to the object store
|
|
||||||
if (env.SELF_HOSTED) {
|
|
||||||
await streamUpload(
|
|
||||||
ObjectStoreBuckets.BACKUPS,
|
|
||||||
join(dbName, exportName),
|
|
||||||
fs.createReadStream(path)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return fs.createReadStream(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue