const path = require('path'); const fs = require('fs'); const express = require('express'); const { default: simpleGit } = require('simple-git'); const sanitize = require('sanitize-filename'); const { PUBLIC_DIRECTORIES } = require('../constants'); const { jsonParser } = require('../express-common'); /** * This function extracts the extension information from the manifest file. * @param {string} extensionPath - The path of the extension folder * @returns {Promise} - Returns the manifest data as an object */ async function getManifest(extensionPath) { const manifestPath = path.join(extensionPath, 'manifest.json'); // Check if manifest.json exists if (!fs.existsSync(manifestPath)) { throw new Error(`Manifest file not found at ${manifestPath}`); } const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); return manifest; } /** * This function checks if the local repository is up-to-date with the remote repository. * @param {string} extensionPath - The path of the extension folder * @returns {Promise} - Returns the extension information as an object */ async function checkIfRepoIsUpToDate(extensionPath) { const git = simpleGit(); await git.cwd(extensionPath).fetch('origin'); const currentBranch = await git.cwd(extensionPath).branch(); const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); const log = await git.cwd(extensionPath).log({ from: currentCommitHash, to: `origin/${currentBranch.current}`, }); // Fetch remote repository information const remotes = await git.cwd(extensionPath).getRemotes(true); return { isUpToDate: log.total === 0, remoteUrl: remotes[0].refs.fetch, // URL of the remote repository }; } const router = express.Router(); /** * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest, * and return extension information and path. * * @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * * @returns {void} */ router.post('/install', jsonParser, 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(); // make sure the third-party directory exists if (!fs.existsSync(path.join(request.user.directories.extensions))) { fs.mkdirSync(path.join(request.user.directories.extensions)); } const url = request.body.url; const extensionPath = path.join(request.user.directories.extensions, path.basename(url, '.git')); if (fs.existsSync(extensionPath)) { return response.status(409).send(`Directory already exists at ${extensionPath}`); } await git.clone(url, extensionPath, { '--depth': 1 }); console.log(`Extension has been cloned at ${extensionPath}`); const { version, author, display_name } = await getManifest(extensionPath); return response.send({ version, author, display_name, extensionPath }); } catch (error) { console.log('Importing custom content failed', error); return response.status(500).send(`Server Error: ${error.message}`); } }); /** * HTTP POST handler function to pull the latest updates from a git repository * based on the extension name provided in the request body. It returns the latest commit hash, * the path of the extension, the status of the repository (whether it's up-to-date or not), * and the remote URL of the repository. * * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * * @returns {void} */ router.post('/update', jsonParser, async (request, response) => { const git = simpleGit(); if (!request.body.extensionName) { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } try { const extensionName = request.body.extensionName; const extensionPath = path.join(request.user.directories.extensions, extensionName); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); } const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); const currentBranch = await git.cwd(extensionPath).branch(); if (!isUpToDate) { await git.cwd(extensionPath).pull('origin', currentBranch.current); console.log(`Extension has been updated at ${extensionPath}`); } else { console.log(`Extension is up to date at ${extensionPath}`); } await git.cwd(extensionPath).fetch('origin'); const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); const shortCommitHash = fullCommitHash.slice(0, 7); return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl }); } catch (error) { console.log('Updating custom content failed', error); return response.status(500).send(`Server Error: ${error.message}`); } }); /** * HTTP POST handler function to get the current git commit hash and branch name for a given extension. * It checks whether the repository is up-to-date with the remote, and returns the status along with * the remote URL of the repository. * * @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * * @returns {void} */ router.post('/version', jsonParser, async (request, response) => { const git = simpleGit(); if (!request.body.extensionName) { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } try { const extensionName = request.body.extensionName; const extensionPath = path.join(request.user.directories.extensions, extensionName); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); } const currentBranch = await git.cwd(extensionPath).branch(); // get only the working branch const currentBranchName = currentBranch.current; await git.cwd(extensionPath).fetch('origin'); const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']); console.log(currentBranch, currentCommitHash); const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl }); } catch (error) { console.log('Getting extension version failed', error); return response.status(500).send(`Server Error: ${error.message}`); } }); /** * HTTP POST handler function to delete a git repository based on the extension name provided in the request body. * * @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property. * @param {Object} response - HTTP Response object used to respond to the HTTP request. * * @returns {void} */ router.post('/delete', jsonParser, async (request, response) => { if (!request.body.extensionName) { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } // Sanitize the extension name to prevent directory traversal const extensionName = sanitize(request.body.extensionName); try { const extensionPath = path.join(request.user.directories.extensions, extensionName); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); } await fs.promises.rm(extensionPath, { recursive: true }); console.log(`Extension has been deleted at ${extensionPath}`); return response.send(`Extension has been deleted at ${extensionPath}`); } catch (error) { console.log('Deleting custom content failed', error); return response.status(500).send(`Server Error: ${error.message}`); } }); /** * Discover the extension folders * If the folder is called third-party, search for subfolders instead */ router.get('/discover', jsonParser, function (request, response) { // get all folders in the extensions folder, except third-party const extensions = fs .readdirSync(PUBLIC_DIRECTORIES.extensions) .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) .filter(f => f !== 'third-party'); // get all folders in the third-party folder, if it exists if (!fs.existsSync(path.join(request.user.directories.extensions))) { return response.send(extensions); } const thirdPartyExtensions = fs .readdirSync(path.join(request.user.directories.extensions)) .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()); // add the third-party extensions to the extensions array extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`)); console.log(extensions); return response.send(extensions); }); module.exports = { router };