Adding plugin upload API - takes a file form-data and then extracts, uploads to minio and stores data about the plugin to CouchDB.

This commit is contained in:
mike12345567 2022-08-10 20:01:48 +01:00
parent f15d3a5b0e
commit a683665a99
10 changed files with 712 additions and 10 deletions

View File

@ -50,6 +50,7 @@ const env = {
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global", GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global",
GLOBAL_CLOUD_BUCKET_NAME: GLOBAL_CLOUD_BUCKET_NAME:
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads", process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
PLUGIN_BUCKET_NAME: process.env.PLUGIN_BUCKET_NAME || "plugins",
USE_COUCH: process.env.USE_COUCH || true, USE_COUCH: process.env.USE_COUCH || true,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,

View File

@ -57,7 +57,11 @@ function publicPolicy(bucketName: any) {
} }
} }
const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] const PUBLIC_BUCKETS = [
ObjectStoreBuckets.APPS,
ObjectStoreBuckets.GLOBAL,
ObjectStoreBuckets.PLUGINS,
]
/** /**
* Gets a connection to the object store using the S3 SDK. * Gets a connection to the object store using the S3 SDK.

View File

@ -8,6 +8,7 @@ exports.ObjectStoreBuckets = {
TEMPLATES: env.TEMPLATES_BUCKET_NAME, TEMPLATES: env.TEMPLATES_BUCKET_NAME,
GLOBAL: env.GLOBAL_BUCKET_NAME, GLOBAL: env.GLOBAL_BUCKET_NAME,
GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME, GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME,
PLUGINS: env.PLUGIN_BUCKET_NAME,
} }
exports.budibaseTempDir = function () { exports.budibaseTempDir = function () {

View File

@ -139,6 +139,7 @@
"snowflake-promise": "^4.5.0", "snowflake-promise": "^4.5.0",
"svelte": "3.49.0", "svelte": "3.49.0",
"swagger-parser": "10.0.3", "swagger-parser": "10.0.3",
"tar": "^6.1.11",
"to-json-schema": "0.2.5", "to-json-schema": "0.2.5",
"uuid": "3.3.2", "uuid": "3.3.2",
"validate.js": "0.13.1", "validate.js": "0.13.1",

View File

@ -0,0 +1,62 @@
import { ObjectStoreBuckets } from "../../constants"
import { extractPluginTarball } from "../../utilities/fileSystem"
import { getGlobalDB } from "@budibase/backend-core/tenancy"
import { generatePluginID } from "../../db/utils"
import { uploadDirectory } from "@budibase/backend-core/objectStore"
export async function upload(ctx: any) {
const plugins =
ctx.request.files.file.length > 1
? Array.from(ctx.request.files.file)
: [ctx.request.files.file]
const db = getGlobalDB()
try {
// can do single or multiple plugins
for (let plugin of plugins) {
const { metadata, directory } = await extractPluginTarball(plugin)
const version = metadata.package.version,
name = metadata.package.name,
description = metadata.package.description
// first open the tarball into tmp directory
const bucketPath = `${name}/${version}/`
const files = await uploadDirectory(
ObjectStoreBuckets.PLUGINS,
directory,
bucketPath
)
const jsFile = files.find((file: any) => file.name.endsWith(".js"))
if (!jsFile) {
throw new Error(`Plugin missing .js file.`)
}
const jsFileName = jsFile.name
const pluginId = generatePluginID(name, version)
let existing
try {
existing = await db.get(pluginId)
} catch (err) {
existing = null
}
if (existing) {
throw new Error(
`Plugin already exists: name: ${name}, version: ${version}`
)
}
await db.put({
_id: pluginId,
name,
version,
description,
...metadata,
jsUrl: `${bucketPath}${jsFileName}`,
})
}
} catch (err: any) {
const errMsg = err?.message ? err?.message : err
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
}
}
export async function fetch(ctx: any) {}
export async function destroy(ctx: any) {}

View File

@ -24,6 +24,7 @@ import metadataRoutes from "./metadata"
import devRoutes from "./dev" import devRoutes from "./dev"
import cloudRoutes from "./cloud" import cloudRoutes from "./cloud"
import migrationRoutes from "./migrations" import migrationRoutes from "./migrations"
import pluginRoutes from "./plugin"
export { default as staticRoutes } from "./static" export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public" export { default as publicRoutes } from "./public"
@ -57,4 +58,5 @@ export const mainRoutes = [
tableRoutes, tableRoutes,
rowRoutes, rowRoutes,
migrationRoutes, migrationRoutes,
pluginRoutes,
] ]

View File

@ -0,0 +1,13 @@
import Router from "@koa/router"
import * as controller from "../controllers/plugin"
import authorized from "../../middleware/authorized"
import { BUILDER } from "@budibase/backend-core/permissions"
const router = new Router()
router
.post("/api/plugin/upload", authorized(BUILDER), controller.upload)
.get("/api/plugin", authorized(BUILDER), controller.fetch)
.delete("/api/plugin/:pluginId", authorized(BUILDER), controller.destroy)
export default router

View File

@ -42,6 +42,7 @@ const DocumentTypes = {
MEM_VIEW: "view", MEM_VIEW: "view",
USER_FLAG: "flag", USER_FLAG: "flag",
AUTOMATION_METADATA: "meta_au", AUTOMATION_METADATA: "meta_au",
PLUGIN: "plg",
} }
const InternalTables = { const InternalTables = {
@ -370,6 +371,10 @@ exports.getMemoryViewParams = (otherProps = {}) => {
return getDocParams(DocumentTypes.MEM_VIEW, null, otherProps) return getDocParams(DocumentTypes.MEM_VIEW, null, otherProps)
} }
exports.generatePluginID = (name, version) => {
return `${DocumentTypes.PLUGIN}${SEPARATOR}${name}${SEPARATOR}${version}`
}
/** /**
* This can be used with the db.allDocs to get a list of IDs * This can be used with the db.allDocs to get a list of IDs
*/ */

View File

@ -25,6 +25,7 @@ const {
} = require("../../db/utils") } = require("../../db/utils")
const MemoryStream = require("memorystream") const MemoryStream = require("memorystream")
const { getAppId } = require("@budibase/backend-core/context") const { getAppId } = require("@budibase/backend-core/context")
const tar = require("tar")
const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..") const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..")
const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules") const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
@ -321,6 +322,32 @@ exports.cleanup = appIds => {
} }
} }
exports.extractPluginTarball = async file => {
if (!file.name.endsWith(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
const path = join(budibaseTempDir(), file.name.split(".tar.gz")[0])
// remove old tmp directories automatically - don't combine
if (fs.existsSync(path)) {
fs.rmSync(path, { recursive: true, force: true })
}
fs.mkdirSync(path)
await tar.extract({
file: file.path,
C: path,
})
let metadata = {}
try {
const pkg = fs.readFileSync(join(path, "package.json"), "utf8")
const schema = fs.readFileSync(join(path, "schema.json"), "utf8")
metadata.schema = JSON.parse(schema)
metadata.package = JSON.parse(pkg)
} catch (err) {
throw new Error("Unable to process schema.json/package.json in plugin.")
}
return { metadata, directory: path }
}
/** /**
* Full function definition for below can be found in the utilities. * Full function definition for below can be found in the utilities.
*/ */

File diff suppressed because it is too large Load Diff