Spaces:
Running
Running
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<Object>} - 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<Object>} - 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 }; | |