| | import path from 'node:path'; |
| | import fs from 'node:fs'; |
| |
|
| | import express from 'express'; |
| | import sanitize from 'sanitize-filename'; |
| | import { CheckRepoActions, default as simpleGit } from 'simple-git'; |
| |
|
| | import { PUBLIC_DIRECTORIES } from '../constants.js'; |
| |
|
| | |
| | |
| | |
| | const OPTIONS = Object.freeze({ timeout: { block: 5 * 60 * 1000 } }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function getManifest(extensionPath) { |
| | const manifestPath = path.join(extensionPath, 'manifest.json'); |
| |
|
| | |
| | if (!fs.existsSync(manifestPath)) { |
| | throw new Error(`Manifest file not found at ${manifestPath}`); |
| | } |
| |
|
| | const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); |
| | return manifest; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function checkIfRepoIsUpToDate(extensionPath) { |
| | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| | await git.fetch('origin'); |
| | const currentBranch = await git.branch(); |
| | const currentCommitHash = await git.revparse(['HEAD']); |
| | const log = await git.log({ |
| | from: currentCommitHash, |
| | to: `origin/${currentBranch.current}`, |
| | }); |
| |
|
| | |
| | const remotes = await git.getRemotes(true); |
| | if (remotes.length === 0) { |
| | return { |
| | isUpToDate: true, |
| | remoteUrl: '', |
| | }; |
| | } |
| |
|
| | return { |
| | isUpToDate: log.total === 0, |
| | remoteUrl: remotes[0].refs.fetch, |
| | }; |
| | } |
| |
|
| | export const router = express.Router(); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | router.post('/install', async (request, response) => { |
| | if (!request.body.url) { |
| | return response.status(400).send('Bad Request: URL is required in the request body.'); |
| | } |
| |
|
| | try { |
| | |
| | const git = simpleGit(); |
| |
|
| | |
| | if (!fs.existsSync(path.join(request.user.directories.extensions))) { |
| | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| | } |
| |
|
| | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { |
| | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| | } |
| |
|
| | const { url, global, branch } = request.body; |
| |
|
| | if (global && !request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to install global extensions.`); |
| | return response.status(403).send('Forbidden: No permission to install global extensions.'); |
| | } |
| |
|
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git'))); |
| |
|
| | if (fs.existsSync(extensionPath)) { |
| | return response.status(409).send(`Directory already exists at ${extensionPath}`); |
| | } |
| |
|
| | const cloneOptions = { '--depth': 1 }; |
| | if (branch) { |
| | cloneOptions['--branch'] = branch; |
| | } |
| | await git.clone(url, extensionPath, cloneOptions); |
| | console.info(`Extension has been cloned to ${extensionPath} from ${url} at ${branch || '(default)'} branch`); |
| |
|
| | const { version, author, display_name } = await getManifest(extensionPath); |
| |
|
| | return response.send({ version, author, display_name, extensionPath }); |
| | } catch (error) { |
| | console.error('Importing custom content failed', error); |
| | return response.status(500).send(`Server Error: ${error.message}`); |
| | } |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | router.post('/update', async (request, response) => { |
| | if (!request.body.extensionName) { |
| | return response.status(400).send('Bad Request: extensionName is required in the request body.'); |
| | } |
| |
|
| | try { |
| | const { extensionName, global } = request.body; |
| |
|
| | if (global && !request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to update global extensions.`); |
| | return response.status(403).send('Forbidden: No permission to update global extensions.'); |
| | } |
| |
|
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(extensionPath)) { |
| | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| | } |
| |
|
| | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); |
| | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| | const isRepo = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| | if (!isRepo) { |
| | throw new Error(`Directory is not a Git repository at ${extensionPath}`); |
| | } |
| | const currentBranch = await git.branch(); |
| | if (!isUpToDate) { |
| | await git.pull('origin', currentBranch.current); |
| | console.info(`Extension has been updated at ${extensionPath}`); |
| | } else { |
| | console.info(`Extension is up to date at ${extensionPath}`); |
| | } |
| | await git.fetch('origin'); |
| | const fullCommitHash = await git.revparse(['HEAD']); |
| | const shortCommitHash = fullCommitHash.slice(0, 7); |
| |
|
| | return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl }); |
| | } catch (error) { |
| | console.error('Updating extension failed', error); |
| | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| | } |
| | }); |
| |
|
| | router.post('/branches', async (request, response) => { |
| | try { |
| | const { extensionName, global } = request.body; |
| |
|
| | if (!extensionName) { |
| | return response.status(400).send('Bad Request: extensionName is required in the request body.'); |
| | } |
| |
|
| | if (global && !request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to list branches of global extensions.`); |
| | return response.status(403).send('Forbidden: No permission to list branches of global extensions.'); |
| | } |
| |
|
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(extensionPath)) { |
| | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| | } |
| |
|
| | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| | |
| | const isShallow = await git.revparse(['--is-shallow-repository']) === 'true'; |
| | if (isShallow) { |
| | console.info(`Unshallowing the repository at ${extensionPath}`); |
| | await git.fetch('origin', ['--unshallow']); |
| | } |
| |
|
| | |
| | await git.remote(['set-branches', 'origin', '*']); |
| | await git.fetch('origin'); |
| | const localBranches = await git.branchLocal(); |
| | const remoteBranches = await git.branch(['-r', '--list', 'origin/*']); |
| | const result = [ |
| | ...Object.values(localBranches.branches), |
| | ...Object.values(remoteBranches.branches), |
| | ].map(b => ({ current: b.current, commit: b.commit, name: b.name, label: b.label })); |
| |
|
| | return response.send(result); |
| | } catch (error) { |
| | console.error('Getting branches failed', error); |
| | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| | } |
| | }); |
| |
|
| | router.post('/switch', async (request, response) => { |
| | try { |
| | const { extensionName, branch, global } = request.body; |
| |
|
| | if (!extensionName || !branch) { |
| | return response.status(400).send('Bad Request: extensionName and branch are required in the request body.'); |
| | } |
| |
|
| | if (global && !request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to switch branches of global extensions.`); |
| | return response.status(403).send('Forbidden: No permission to switch branches of global extensions.'); |
| | } |
| |
|
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(extensionPath)) { |
| | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| | } |
| |
|
| | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| | const branches = await git.branchLocal(); |
| |
|
| | if (String(branch).startsWith('origin/')) { |
| | const localBranch = branch.replace('origin/', ''); |
| | if (branches.all.includes(localBranch)) { |
| | console.info(`Branch ${localBranch} already exists locally, checking it out`); |
| | await git.checkout(localBranch); |
| | return response.sendStatus(204); |
| | } |
| |
|
| | console.info(`Branch ${localBranch} does not exist locally, creating it from ${branch}`); |
| | await git.checkoutBranch(localBranch, branch); |
| | return response.sendStatus(204); |
| | } |
| |
|
| | if (!branches.all.includes(branch)) { |
| | console.error(`Branch ${branch} does not exist locally`); |
| | return response.status(404).send(`Branch ${branch} does not exist locally`); |
| | } |
| |
|
| | |
| | const currentBranch = await git.branch(); |
| | if (currentBranch.current === branch) { |
| | console.info(`Branch ${branch} is already checked out`); |
| | return response.sendStatus(204); |
| | } |
| |
|
| | |
| | await git.checkout(branch); |
| | console.info(`Checked out branch ${branch} at ${extensionPath}`); |
| |
|
| | return response.sendStatus(204); |
| | } catch (error) { |
| | console.error('Switching branches failed', error); |
| | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| | } |
| | }); |
| |
|
| | router.post('/move', async (request, response) => { |
| | try { |
| | const { extensionName, source, destination } = request.body; |
| |
|
| | if (!extensionName || !source || !destination) { |
| | return response.status(400).send('Bad Request. Not all required parameters are provided.'); |
| | } |
| |
|
| | if (!request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to move extensions.`); |
| | return response.status(403).send('Forbidden: No permission to move extensions.'); |
| | } |
| |
|
| | const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const sourcePath = path.join(sourceDirectory, sanitize(extensionName)); |
| | const destinationPath = path.join(destinationDirectory, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { |
| | console.error(`Source directory does not exist at ${sourcePath}`); |
| | return response.status(404).send('Source directory does not exist.'); |
| | } |
| |
|
| | if (fs.existsSync(destinationPath)) { |
| | console.error(`Destination directory already exists at ${destinationPath}`); |
| | return response.status(409).send('Destination directory already exists.'); |
| | } |
| |
|
| | if (source === destination) { |
| | console.error('Source and destination directories are the same'); |
| | return response.status(409).send('Source and destination directories are the same.'); |
| | } |
| |
|
| | fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); |
| | fs.rmSync(sourcePath, { recursive: true, force: true }); |
| | console.info(`Extension has been moved from ${sourcePath} to ${destinationPath}`); |
| |
|
| | return response.sendStatus(204); |
| | } catch (error) { |
| | console.error('Moving extension failed', error); |
| | return response.status(500).send('Internal Server Error. Check the server logs for more details.'); |
| | } |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | router.post('/version', async (request, response) => { |
| | if (!request.body.extensionName) { |
| | return response.status(400).send('Bad Request: extensionName is required in the request body.'); |
| | } |
| |
|
| | try { |
| | const { extensionName, global } = request.body; |
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(extensionPath)) { |
| | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| | } |
| |
|
| | const git = simpleGit({ baseDir: extensionPath, ...OPTIONS }); |
| | let currentCommitHash; |
| | try { |
| | const isRepo = await git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); |
| | if (!isRepo) { |
| | throw new Error(`Directory is not a Git repository at ${extensionPath}`); |
| | } |
| | currentCommitHash = await git.revparse(['HEAD']); |
| | } catch (error) { |
| | |
| | |
| | return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' }); |
| | } |
| |
|
| | const currentBranch = await git.branch(); |
| | |
| | const currentBranchName = currentBranch.current; |
| | await git.fetch('origin'); |
| | console.debug(extensionName, currentBranchName, currentCommitHash); |
| | const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); |
| |
|
| | return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl }); |
| |
|
| | } catch (error) { |
| | console.error('Getting extension version failed', error); |
| | return response.status(500).send(`Server Error: ${error.message}`); |
| | } |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | router.post('/delete', async (request, response) => { |
| | if (!request.body.extensionName) { |
| | return response.status(400).send('Bad Request: extensionName is required in the request body.'); |
| | } |
| |
|
| | try { |
| | const { extensionName, global } = request.body; |
| |
|
| | if (global && !request.user.profile.admin) { |
| | console.error(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); |
| | return response.status(403).send('Forbidden: No permission to delete global extensions.'); |
| | } |
| |
|
| | const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; |
| | const extensionPath = path.join(basePath, sanitize(extensionName)); |
| |
|
| | if (!fs.existsSync(extensionPath)) { |
| | return response.status(404).send(`Directory does not exist at ${extensionPath}`); |
| | } |
| |
|
| | await fs.promises.rm(extensionPath, { recursive: true }); |
| | console.info(`Extension has been deleted at ${extensionPath}`); |
| |
|
| | return response.send(`Extension has been deleted at ${extensionPath}`); |
| |
|
| | } catch (error) { |
| | console.error('Deleting custom content failed', error); |
| | return response.status(500).send(`Server Error: ${error.message}`); |
| | } |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | router.get('/discover', function (request, response) { |
| | if (!fs.existsSync(path.join(request.user.directories.extensions))) { |
| | fs.mkdirSync(path.join(request.user.directories.extensions)); |
| | } |
| |
|
| | if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { |
| | fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); |
| | } |
| |
|
| | |
| | const builtInExtensions = fs |
| | .readdirSync(PUBLIC_DIRECTORIES.extensions) |
| | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) |
| | .filter(f => f !== 'third-party') |
| | .map(f => ({ type: 'system', name: f })); |
| |
|
| | |
| | const userExtensions = fs |
| | .readdirSync(path.join(request.user.directories.extensions)) |
| | .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) |
| | .map(f => ({ type: 'local', name: `third-party/${f}` })); |
| |
|
| | |
| | |
| | const globalExtensions = fs |
| | .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) |
| | .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) |
| | .map(f => ({ type: 'global', name: `third-party/${f}` })) |
| | .filter(f => !userExtensions.some(e => e.name === f.name)); |
| |
|
| | |
| | const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions]; |
| | console.debug('Extensions available for', request.user.profile.handle, allExtensions); |
| |
|
| | return response.send(allExtensions); |
| | }); |
| |
|