mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-15 10:48:36 +01:00
ea36d34942
It will fail anyway if not installed
252 lines
10 KiB
JavaScript
252 lines
10 KiB
JavaScript
const path = require('path');
|
|
const fs = require('fs');
|
|
const { default: simpleGit } = require('simple-git');
|
|
const sanitize = require('sanitize-filename');
|
|
const commandExistsSync = require('command-exists').sync;
|
|
const { DIRECTORIES } = require('./constants');
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Registers the endpoints for the third-party extensions API.
|
|
* @param {import('express').Express} app - Express app
|
|
* @param {any} jsonParser - JSON parser middleware
|
|
*/
|
|
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.');
|
|
}
|
|
|
|
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) => {
|
|
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
|
|
|
|
if (!fs.existsSync(path.join(DIRECTORIES.extensions, 'third-party'))) {
|
|
return response.send(extensions);
|
|
}
|
|
|
|
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 = {
|
|
registerEndpoints,
|
|
}
|