Use Express router for extensions endpoint

This commit is contained in:
valadaptive 2023-12-04 12:48:29 -05:00
parent 414c9bd5fb
commit babe9abbe9
2 changed files with 185 additions and 191 deletions

View File

@ -3594,7 +3594,7 @@ require('./src/endpoints/thumbnails').registerEndpoints(app, jsonParser);
require('./src/endpoints/novelai').registerEndpoints(app, jsonParser);
// Third-party extensions
require('./src/endpoints/extensions').registerEndpoints(app, jsonParser);
app.use('/api/extensions', require('./src/endpoints/extensions').router);
// Asset management
require('./src/endpoints/assets').registerEndpoints(app, jsonParser);

View File

@ -1,8 +1,10 @@
const path = require('path');
const fs = require('fs');
const express = require('express');
const { default: simpleGit } = require('simple-git');
const sanitize = require('sanitize-filename');
const { DIRECTORIES } = require('../constants');
const { jsonParser } = require('../express-common');
/**
* This function extracts the extension information from the manifest file.
@ -45,206 +47,198 @@ async function checkIfRepoIsUpToDate(extensionPath) {
};
}
const router = express.Router();
/**
* Registers the endpoints for the third-party extensions API.
* @param {import('express').Express} app - Express app
* @param {any} jsonParser - JSON parser middleware
* 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}
*/
function registerEndpoints(app, jsonParser) {
/**
* 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}
*/
app.post('/api/extensions/install', jsonParser, async (request, response) => {
if (!request.body.url) {
return response.status(400).send('Bad Request: URL is required in the request body.');
}
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(DIRECTORIES.extensions, 'third-party'))) {
fs.mkdirSync(path.join(DIRECTORIES.extensions, 'third-party'));
}
const url = request.body.url;
const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', 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}
*/
app.post('/api/extensions/update', jsonParser, async (request, response) => {
try {
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(DIRECTORIES.extensions, 'third-party', 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}
*/
app.post('/api/extensions/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(DIRECTORIES.extensions, 'third-party', 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}
*/
app.post('/api/extensions/delete', jsonParser, async (request, response) => {
if (!request.body.extensionName) {
return response.status(400).send('Bad Request: extensionName is required in the request body.');
}
// Sanatize the extension name to prevent directory traversal
const extensionName = sanitize(request.body.extensionName);
try {
const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', extensionName);
if (!fs.existsSync(extensionPath)) {
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
}
await fs.promises.rmdir(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
*/
app.get('/api/extensions/discover', jsonParser, function (_, response) {
// get all folders in the extensions folder, except third-party
const extensions = fs
.readdirSync(DIRECTORIES.extensions)
.filter(f => fs.statSync(path.join(DIRECTORIES.extensions, f)).isDirectory())
.filter(f => f !== 'third-party');
// get all folders in the third-party folder, if it exists
// make sure the third-party directory exists
if (!fs.existsSync(path.join(DIRECTORIES.extensions, 'third-party'))) {
return response.send(extensions);
fs.mkdirSync(path.join(DIRECTORIES.extensions, 'third-party'));
}
const thirdPartyExtensions = fs
.readdirSync(path.join(DIRECTORIES.extensions, 'third-party'))
.filter(f => fs.statSync(path.join(DIRECTORIES.extensions, 'third-party', f)).isDirectory());
const url = request.body.url;
const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', path.basename(url, '.git'));
// add the third-party extensions to the extensions array
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
console.log(extensions);
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(DIRECTORIES.extensions, 'third-party', 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(DIRECTORIES.extensions, 'third-party', 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.');
}
// Sanatize the extension name to prevent directory traversal
const extensionName = sanitize(request.body.extensionName);
try {
const extensionPath = path.join(DIRECTORIES.extensions, 'third-party', extensionName);
if (!fs.existsSync(extensionPath)) {
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
}
await fs.promises.rmdir(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('/api/extensions/discover', jsonParser, function (_, response) {
// get all folders in the extensions folder, except third-party
const extensions = fs
.readdirSync(DIRECTORIES.extensions)
.filter(f => fs.statSync(path.join(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(DIRECTORIES.extensions, 'third-party'))) {
return response.send(extensions);
});
}
}
module.exports = {
registerEndpoints,
};
const thirdPartyExtensions = fs
.readdirSync(path.join(DIRECTORIES.extensions, 'third-party'))
.filter(f => fs.statSync(path.join(DIRECTORIES.extensions, 'third-party', 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 };