From 6716bf2da18c08944b768760fdfb9cf0b34bf719 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 8 Jul 2021 12:56:54 +0100 Subject: [PATCH] Add endpoint to revert client app version --- .../server/src/api/controllers/application.js | 36 ++++- packages/server/src/api/routes/application.js | 5 + .../src/utilities/fileSystem/clientLibrary.js | 149 ++++++++++++++++++ .../server/src/utilities/fileSystem/index.js | 6 +- .../server/src/utilities/fileSystem/newApp.js | 27 ---- 5 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 packages/server/src/utilities/fileSystem/clientLibrary.js delete mode 100644 packages/server/src/utilities/fileSystem/newApp.js diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index e8ace42bcd..d8e1e232d3 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -34,9 +34,10 @@ const { const { clientLibraryPath } = require("../../utilities") const { getAllLocks } = require("../../utilities/redis") const { - uploadClientLibrary, - downloadLibraries, -} = require("../../utilities/fileSystem/newApp") + updateClientLibrary, + backupClientLibrary, + revertClientLibrary, +} = require("../../utilities/fileSystem/clientLibrary") const URL_REGEX_SLASH = /\/|\\/g @@ -247,7 +248,8 @@ exports.updateClient = async function (ctx) { const currentVersion = application.version // Update client library and manifest - await uploadClientLibrary(ctx.params.appId) + await backupClientLibrary(ctx.params.appId) + await updateClientLibrary(ctx.params.appId) // Update versions in app package const appPackageUpdates = { @@ -259,6 +261,27 @@ exports.updateClient = async function (ctx) { ctx.body = data } +exports.revertClient = async function (ctx) { + // Check app can be reverted + const db = new CouchDB(ctx.params.appId) + const application = await db.get(DocumentTypes.APP_METADATA) + if (!application.revertableVersion) { + ctx.throw(400, "There is no version to revert to") + } + + // Update client library and manifest + await revertClientLibrary(ctx.params.appId) + + // Update versions in app package + const appPackageUpdates = { + version: application.revertableVersion, + revertableVersion: null, + } + const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId) + ctx.status = 200 + ctx.body = data +} + exports.delete = async function (ctx) { const db = new CouchDB(ctx.params.appId) @@ -290,10 +313,7 @@ const updateAppPackage = async (ctx, appPackage, appId) => { delete newAppPackage.lockedBy } - const response = await db.put(newAppPackage) - console.log(response) - - return response + return await db.put(newAppPackage) } const createEmptyAppPackage = async (ctx, app) => { diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index 6ffd3a4b5b..c2eb19e101 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -16,6 +16,11 @@ router authorized(BUILDER), controller.updateClient ) + .post( + "/api/applications/:appId/client/revert", + authorized(BUILDER), + controller.revertClient + ) .delete("/api/applications/:appId", authorized(BUILDER), controller.delete) module.exports = router diff --git a/packages/server/src/utilities/fileSystem/clientLibrary.js b/packages/server/src/utilities/fileSystem/clientLibrary.js new file mode 100644 index 0000000000..6b7e8d837d --- /dev/null +++ b/packages/server/src/utilities/fileSystem/clientLibrary.js @@ -0,0 +1,149 @@ +const { join } = require("path") +const { ObjectStoreBuckets } = require("../../constants") +const fs = require("fs") +const { upload, retrieveToTmp, streamUpload } = require("./utilities") + +/** + * Client library paths in the object store: + * Previously, the entire standard-components package was downloaded from NPM + * as a tarball and extracted to the object store, even though only the manifest + * was ever needed. Therefore we need to support old apps which may still have + * the manifest at this location for the first update. + * + * The new paths for the in-use version are: + * {appId}/manifest.json + * {appId}/budibase-client.js + * + * The paths for the backups are: + * {appId}/manifest.json.bak + * {appId}/budibase-client.js.bak + * + * We don't rely on NPM at all any more, as when updating to the latest version + * we pull both the manifest and client bundle from the server's dependencies + * in the local file system. + */ + +/** + * Backs up the current client library version by copying both the manifest + * and client bundle to .bak extensions in the object store. Only the one + * previous version is stored as a backup, which can be reverted to. + * @param appId The app ID to backup + * @returns {Promise} + */ +exports.backupClientLibrary = async appId => { + let tmpManifestPath + let tmpClientPath + + // Copy existing manifest to tmp + try { + // Try to load the manifest from the new file location + tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json") + ) + } catch (error) { + // Fallback to loading it from the old location for old apps + tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join( + appId, + "node_modules", + "budibase", + "standard-components", + "package", + "manifest.json" + ) + ) + } + + // Copy existing client lib to tmp + tmpClientPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js") + ) + + // Upload manifest as backup + await upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "manifest.json.bak"), + path: tmpManifestPath, + type: "application/json", + }) + + // Upload client library as backup + await upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "budibase-client.js.bak"), + path: tmpClientPath, + type: "application/javascript", + }) +} + +/** + * Uploads the latest version of the component manifest and the client library + * to the object store, overwriting the existing version. + * @param appId The app ID to update + * @returns {Promise} + */ +exports.updateClientLibrary = async appId => { + // Upload latest component manifest + await streamUpload( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json"), + fs.createReadStream( + require.resolve("@budibase/standard-components/manifest.json") + ), + { + ContentType: "application/json", + } + ) + + // Upload latest component library + await streamUpload( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js"), + fs.createReadStream(require.resolve("@budibase/client")), + { + ContentType: "application/javascript", + } + ) +} + +/** + * Reverts the version of the client library and manifest to the previously + * used version for an app. + * @param appId The app ID to revert + * @returns {Promise} + */ +exports.revertClientLibrary = async appId => { + let tmpManifestPath + let tmpClientPath + + // Copy backup manifest to tmp + tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json.bak") + ) + + // Copy backup client lib to tmp + tmpClientPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js.bak") + ) + + // Upload manifest backup + await upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "manifest.json"), + path: tmpManifestPath, + type: "application/json", + }) + + // Upload client library backup + await upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "budibase-client.js"), + path: tmpClientPath, + type: "application/javascript", + }) +} diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index c64d83dd67..b83ff03854 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -13,7 +13,7 @@ const { deleteFolder, downloadTarball, } = require("./utilities") -const { uploadClientLibrary } = require("./newApp") +const { updateClientLibrary } = require("./clientLibrary") const download = require("download") const env = require("../../environment") const { homedir } = require("os") @@ -139,12 +139,12 @@ exports.performBackup = async (appId, backupName) => { } /** - * Downloads required libraries and creates a new path in the object store. + * Uploads the latest client library to the object store. * @param {string} appId The ID of the app which is being created. * @return {Promise} once promise completes app resources should be ready in object store. */ exports.createApp = async appId => { - await uploadClientLibrary(appId) + await updateClientLibrary(appId) } /** diff --git a/packages/server/src/utilities/fileSystem/newApp.js b/packages/server/src/utilities/fileSystem/newApp.js deleted file mode 100644 index 749e7a278d..0000000000 --- a/packages/server/src/utilities/fileSystem/newApp.js +++ /dev/null @@ -1,27 +0,0 @@ -const { join } = require("path") -const { ObjectStoreBuckets } = require("../../constants") -const { streamUpload } = require("./utilities") -const fs = require("fs") - -const BUCKET_NAME = ObjectStoreBuckets.APPS - -exports.uploadClientLibrary = async appId => { - await streamUpload( - BUCKET_NAME, - join(appId, "budibase-client.js"), - fs.createReadStream(require.resolve("@budibase/client")), - { - ContentType: "application/javascript", - } - ) - await streamUpload( - BUCKET_NAME, - join(appId, "manifest.json"), - fs.createReadStream( - require.resolve("@budibase/standard-components/manifest.json") - ), - { - ContentType: "application/javascript", - } - ) -}