uploading plugin for github, npm and url

This commit is contained in:
NEOLPAR 2022-09-06 16:28:35 +01:00
parent 32863caf05
commit 62501bbef7
3 changed files with 229 additions and 144 deletions

View File

@ -1,11 +1,11 @@
import { ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import { loadJSFile } from "../../utilities/fileSystem"
import { import {
extractPluginTarball, uploadedNpmPlugin,
createUrlPlugin, uploadedUrlPlugin,
createGithubPlugin, uploadedGithubPlugin,
loadJSFile, uploadedFilePlugin,
} from "../../utilities/fileSystem" } from "./plugin/utils"
import { createNpmPlugin } from "./plugin/utils"
import { getGlobalDB } from "@budibase/backend-core/tenancy" import { getGlobalDB } from "@budibase/backend-core/tenancy"
import { generatePluginID, getPluginParams } from "../../db/utils" import { generatePluginID, getPluginParams } from "../../db/utils"
import { import {
@ -60,22 +60,32 @@ export async function create(ctx: any) {
// Generating random name as a backup and needed for url // Generating random name as a backup and needed for url
let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000) let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
if (!env.SELF_HOSTED) {
throw new Error("Plugins not supported outside of self-host.")
}
switch (source) { switch (source) {
case "npm": case "npm":
try {
const { metadata: metadataNpm, directory: directoryNpm } = const { metadata: metadataNpm, directory: directoryNpm } =
await createNpmPlugin(url, name) await uploadedNpmPlugin(url, name)
metadata = metadataNpm metadata = metadataNpm
directory = directoryNpm directory = directoryNpm
} catch (err: any) {
const errMsg = err?.message ? err?.message : err
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
}
break break
case "github": case "github":
const { metadata: metadataGithub, directory: directoryGithub } = const { metadata: metadataGithub, directory: directoryGithub } =
await createGithubPlugin(ctx, url, name, githubToken) await uploadedGithubPlugin(ctx, url, name, githubToken)
metadata = metadataGithub metadata = metadataGithub
directory = directoryGithub directory = directoryGithub
break break
case "url": case "url":
const { metadata: metadataUrl, directory: directoryUrl } = const { metadata: metadataUrl, directory: directoryUrl } =
await createUrlPlugin(url, name, headers) await uploadedUrlPlugin(url, name, headers)
metadata = metadataUrl metadata = metadataUrl
directory = directoryUrl directory = directoryUrl
break break
@ -192,6 +202,6 @@ export async function processPlugin(plugin: FileType, source?: string) {
throw new Error("Plugins not supported outside of self-host.") throw new Error("Plugins not supported outside of self-host.")
} }
const { metadata, directory } = await extractPluginTarball(plugin) const { metadata, directory } = await uploadedFilePlugin(plugin)
return await storePlugin(metadata, directory, source) return await storePlugin(metadata, directory, source)
} }

View File

@ -1,32 +1,153 @@
const fetch = require("node-fetch") import fetch from "node-fetch"
import { downloadUnzipPlugin } from "../../../utilities/fileSystem" import {
createTempFolder,
getPluginMetadata,
findFileRec,
downloadTarballDirect,
extractTarball,
deleteFolderFileSystem,
} from "../../../utilities/fileSystem"
import { join } from "path"
export const createNpmPlugin = async (url, name = "") => { export const uploadedFilePlugin = async file => {
let npmTarball = url if (!file.name.endsWith(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
const path = createTempFolder(file.name.split(".tar.gz")[0])
await extractTarball(file.path, path)
return await getPluginMetadata(path)
}
export const uploadedNpmPlugin = async (url, name, headers = {}) => {
let npmTarballUrl = url
let pluginName = name let pluginName = name
if ( if (
!npmTarball.includes("https://www.npmjs.com") && !npmTarballUrl.includes("https://www.npmjs.com") &&
!npmTarball.includes("https://registry.npmjs.org") !npmTarballUrl.includes("https://registry.npmjs.org")
) { ) {
throw "The plugin origin must be from NPM" throw new Error("The plugin origin must be from NPM")
} }
if (!npmTarball.includes(".tgz")) { if (!npmTarballUrl.includes(".tgz")) {
const npmPackageURl = url.replace( const npmPackageURl = url.replace(
"https://www.npmjs.com/package/", "https://www.npmjs.com/package/",
"https://registry.npmjs.org/" "https://registry.npmjs.org/"
) )
const response = await fetch(npmPackageURl) const response = await fetch(npmPackageURl)
if (response.status === 200) { if (response.status !== 200) {
throw new Error("NPM Package not found")
}
let npmDetails = await response.json() let npmDetails = await response.json()
pluginName = npmDetails.name pluginName = npmDetails.name
const npmVersion = npmDetails["dist-tags"].latest const npmVersion = npmDetails["dist-tags"].latest
npmTarball = npmDetails.versions[npmVersion].dist.tarball npmTarballUrl = npmDetails?.versions?.[npmVersion]?.dist?.tarball
} else {
throw "Cannot get package details" if (!npmTarballUrl) {
throw new Error("NPM tarball url not found")
} }
} }
return await downloadUnzipPlugin(pluginName, npmTarball) const path = await downloadUnzipTarball(npmTarballUrl, pluginName, headers)
const tarballPluginFile = findFileRec(path, ".tar.gz")
if (!tarballPluginFile) {
throw new Error("Tarball plugin file not found")
}
try {
await extractTarball(tarballPluginFile, path)
deleteFolderFileSystem(join(path, "package"))
} catch (err) {
throw new Error(err)
}
return await getPluginMetadata(path)
}
export const uploadedUrlPlugin = async (url, name = "", headers = {}) => {
if (!url.includes(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
const path = await downloadUnzipTarball(url, name, headers)
return await getPluginMetadata(path)
}
export const uploadedGithubPlugin = async (ctx, url, name = "", token = "") => {
let githubUrl = url
if (!githubUrl.includes("https://github.com/")) {
throw new Error("The plugin origin must be from Github")
}
if (url.includes(".git")) {
githubUrl = url.replace(".git", "")
}
const githubApiUrl = githubUrl.replace(
"https://github.com/",
"https://api.github.com/repos/"
)
const headers = token ? { Authorization: `Bearer ${token}` } : {}
try {
const pluginRaw = await fetch(githubApiUrl, { headers })
if (pluginRaw.status !== 200) {
throw new Error(`Repository not found`)
}
let pluginDetails = await pluginRaw.json()
const pluginName = pluginDetails.name || name
const pluginLatestReleaseUrl = pluginDetails?.["releases_url"]
? pluginDetails?.["releases_url"].replace("{/id}", "/latest")
: undefined
if (!pluginLatestReleaseUrl) {
throw new Error("Github release not found")
}
const pluginReleaseRaw = await fetch(pluginLatestReleaseUrl, { headers })
if (pluginReleaseRaw.status !== 200) {
throw new Error("Github latest release not found")
}
const pluginReleaseDetails = await pluginReleaseRaw.json()
const pluginReleaseTarballAsset = pluginReleaseDetails?.assets?.find(
x => x?.content_type === "application/gzip"
)
const pluginLastReleaseTarballUrl =
pluginReleaseTarballAsset?.browser_download_url
if (!pluginLastReleaseTarballUrl) {
throw new Error("Github latest release url not found")
}
const path = await downloadUnzipTarball(
pluginLastReleaseTarballUrl,
pluginName,
headers
)
return await getPluginMetadata(path)
} catch (err) {
let errMsg = err?.message || err
if (errMsg === "unexpected response Not Found") {
errMsg = "Github release tarbal not found"
}
throw new Error(errMsg)
}
}
export const downloadUnzipTarball = async (url, name, headers = {}) => {
try {
const path = createTempFolder(name)
await downloadTarballDirect(url, path, headers)
return path
} catch (e) {
throw new Error(e.message)
}
} }

View File

@ -1,8 +1,6 @@
const { budibaseTempDir } = require("../budibaseDir") const { budibaseTempDir } = require("../budibaseDir")
const fs = require("fs") const fs = require("fs")
const { join } = require("path") const { join } = require("path")
const { promisify } = require("util")
const streamPipeline = promisify(require("stream").pipeline)
const uuid = require("uuid/v4") const uuid = require("uuid/v4")
const { const {
doWithDB, doWithDB,
@ -17,6 +15,7 @@ const {
streamUpload, streamUpload,
deleteFolder, deleteFolder,
downloadTarball, downloadTarball,
downloadTarballDirect,
deleteFiles, deleteFiles,
} = require("./utilities") } = require("./utilities")
const { updateClientLibrary } = require("./clientLibrary") const { updateClientLibrary } = require("./clientLibrary")
@ -31,7 +30,6 @@ const MemoryStream = require("memorystream")
const { getAppId } = require("@budibase/backend-core/context") const { getAppId } = require("@budibase/backend-core/context")
const tar = require("tar") const tar = require("tar")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const { NodeVM } = require("vm2")
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")
@ -341,131 +339,52 @@ exports.cleanup = appIds => {
} }
} }
const extractPluginTarball = async (file, ext = ".tar.gz") => { const createTempFolder = item => {
if (!file.name.endsWith(ext)) { const path = join(budibaseTempDir(), item)
throw new Error("Plugin must be compressed into a gzipped tarball.") try {
}
const path = join(budibaseTempDir(), file.name.split(ext)[0])
// remove old tmp directories automatically - don't combine // remove old tmp directories automatically - don't combine
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
fs.rmSync(path, { recursive: true, force: true }) fs.rmSync(path, { recursive: true, force: true })
} }
fs.mkdirSync(path) fs.mkdirSync(path)
} catch (err) {
throw new Error(`Path cannot be created: ${err.message}`)
}
return path
}
exports.createTempFolder = createTempFolder
const extractTarball = async (fromFilePath, toPath) => {
await tar.extract({ await tar.extract({
file: file.path, file: fromFilePath,
C: path, C: toPath,
}) })
return await getPluginMetadata(path)
} }
exports.extractPluginTarball = extractPluginTarball exports.extractTarball = extractTarball
exports.createUrlPlugin = async (url, name = "", headers = {}) => {
if (!url.includes(".tgz") && !url.includes(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
return await downloadUnzipPlugin(name, url, headers)
}
exports.createGithubPlugin = async (ctx, url, name = "", token = "") => {
let githubRepositoryUrl
let githubUrl
if (url.includes(".git")) {
githubRepositoryUrl = token
? url.replace("https://", `https://${token}@`)
: url
githubUrl = url.replace(".git", "")
} else {
githubRepositoryUrl = token
? `${url}.git`.replace("https://", `https://${token}@`)
: `${url}.git`
githubUrl = url
}
const githubApiUrl = githubUrl.replace(
"https://github.com/",
"https://api.github.com/repos/"
)
const headers = token ? { Authorization: `Bearer ${token}` } : {}
try {
const pluginRaw = await fetch(githubApiUrl, { headers })
if (pluginRaw.status !== 200) {
throw `Repository not found`
}
let pluginDetails = await pluginRaw.json()
const pluginName = pluginDetails.name || name
const path = join(budibaseTempDir(), pluginName)
// Remove first if exists
if (fs.existsSync(path)) {
fs.rmSync(path, { recursive: true, force: true })
}
fs.mkdirSync(path)
const script = `
module.exports = async () => {
const child_process = require('child_process')
child_process.execSync(\`git clone ${githubRepositoryUrl} ${join(
budibaseTempDir(),
pluginName
)}\`);
}
`
const scriptRunner = new NodeVM({
require: {
external: true,
builtin: ["child_process"],
root: "./",
},
}).run(script)
await scriptRunner()
return await getPluginMetadata(path)
} catch (e) {
throw e.message
}
}
const downloadUnzipPlugin = async (name, url, headers = {}) => {
const path = join(budibaseTempDir(), name)
try {
// Remove first if exists
if (fs.existsSync(path)) {
fs.rmSync(path, { recursive: true, force: true })
}
fs.mkdirSync(path)
const response = await fetch(url, { headers })
if (!response.ok)
throw new Error(`Loading NPM plugin failed ${response.statusText}`)
await streamPipeline(
response.body,
tar.x({
strip: 1,
C: path,
})
)
return await getPluginMetadata(path)
} catch (e) {
throw `Cannot store plugin locally: ${e.message}`
}
}
exports.downloadUnzipPlugin = downloadUnzipPlugin
const getPluginMetadata = async path => { const getPluginMetadata = async path => {
let metadata = {} let metadata = {}
try { try {
const pkg = fs.readFileSync(join(path, "package.json"), "utf8") const pkg = fs.readFileSync(join(path, "package.json"), "utf8")
const schema = fs.readFileSync(join(path, "schema.json"), "utf8") const schema = fs.readFileSync(join(path, "schema.json"), "utf8")
metadata.schema = JSON.parse(schema) metadata.schema = JSON.parse(schema)
metadata.package = JSON.parse(pkg) metadata.package = JSON.parse(pkg)
if (
!metadata.package.name ||
!metadata.package.version ||
!metadata.package.description
) {
throw new Error(
"package.json is missing one of 'name', 'version' or 'description'."
)
}
} catch (err) { } catch (err) {
throw new Error("Unable to process schema.json/package.json in plugin.") throw new Error(
`Unable to process schema.json/package.json in plugin. ${err.message}`
)
} }
return { metadata, directory: path } return { metadata, directory: path }
@ -504,6 +423,40 @@ exports.getDatasourcePlugin = async (name, url, hash) => {
} }
} }
/**
* Find for a file recursively from start path applying filter, return first match
*/
const findFileRec = (startPath, filter) => {
if (!fs.existsSync(startPath)) {
return
}
var files = fs.readdirSync(startPath)
for (let i = 0, len = files.length; i < len; i++) {
const filename = join(startPath, files[i])
const stat = fs.lstatSync(filename)
if (stat.isDirectory()) {
return findFileRec(filename, filter)
} else if (filename.endsWith(filter)) {
return filename
}
}
}
exports.findFileRec = findFileRec
/**
* Remove a folder which is not empty from the file system
*/
const deleteFolderFileSystem = path => {
if (!fs.existsSync(path)) {
return
}
fs.rmSync(path, { recursive: true, force: true })
}
exports.deleteFolderFileSystem = deleteFolderFileSystem
/** /**
* Full function definition for below can be found in the utilities. * Full function definition for below can be found in the utilities.
*/ */
@ -511,5 +464,6 @@ exports.upload = upload
exports.retrieve = retrieve exports.retrieve = retrieve
exports.retrieveToTmp = retrieveToTmp exports.retrieveToTmp = retrieveToTmp
exports.deleteFiles = deleteFiles exports.deleteFiles = deleteFiles
exports.downloadTarballDirect = downloadTarballDirect
exports.TOP_LEVEL_PATH = TOP_LEVEL_PATH exports.TOP_LEVEL_PATH = TOP_LEVEL_PATH
exports.NODE_MODULES_PATH = NODE_MODULES_PATH exports.NODE_MODULES_PATH = NODE_MODULES_PATH