|
|
@@ -9,7 +9,7 @@ import streamToPromise from 'stream-to-promise';
|
|
|
import unzipper from 'unzipper';
|
|
|
|
|
|
import {
|
|
|
- GrowiPlugin, GrowiPluginOrigin, GrowiPluginResourceType, GrowiThemePluginMeta,
|
|
|
+ GrowiPlugin, GrowiPluginOrigin, GrowiPluginResourceType, GrowiThemePluginMeta, GrowiPluginMeta,
|
|
|
} from '~/interfaces/plugin';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
import { resolveFromRoot } from '~/utils/project-dir-utils';
|
|
|
@@ -42,19 +42,22 @@ export interface IPluginService {
|
|
|
|
|
|
export class PluginService implements IPluginService {
|
|
|
|
|
|
+ /*
|
|
|
+ * Downloading a non-existent repository to the file system
|
|
|
+ */
|
|
|
async downloadNotExistPluginRepositories(): Promise<void> {
|
|
|
try {
|
|
|
- // check all growi plugin documents
|
|
|
+ // find all growi plugin documents
|
|
|
const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
|
|
|
const growiPlugins = await GrowiPlugin.find({});
|
|
|
+
|
|
|
+ // if not exists repository in file system, download latest plugin repository
|
|
|
for await (const growiPlugin of growiPlugins) {
|
|
|
const pluginPath = path.join(pluginStoringPath, growiPlugin.installedPath);
|
|
|
if (fs.existsSync(pluginPath)) {
|
|
|
- // if exists repository, do nothing
|
|
|
continue;
|
|
|
}
|
|
|
else {
|
|
|
- // if not exists repository, download latest plugin repository
|
|
|
// TODO: imprv Document version and repository version possibly different.
|
|
|
const ghUrl = new URL(growiPlugin.origin.url);
|
|
|
const ghPathname = ghUrl.pathname;
|
|
|
@@ -68,8 +71,24 @@ export class PluginService implements IPluginService {
|
|
|
const ghOrganizationName = match[1];
|
|
|
const ghReposName = match[2];
|
|
|
|
|
|
- // download github repository to local file system
|
|
|
- await this.downloadPluginRepository(ghOrganizationName, ghReposName, ghBranch);
|
|
|
+ const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
|
|
|
+ const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
|
|
|
+ const unzippedPath = pluginStoringPath;
|
|
|
+ const unzippedReposPath = path.join(pluginStoringPath, `${ghReposName}-${ghBranch}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // download github repository to local file system
|
|
|
+ await this.download(requestUrl, zipFilePath);
|
|
|
+ await this.unzip(zipFilePath, unzippedPath);
|
|
|
+ fs.renameSync(unzippedReposPath, pluginPath);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ // clean up, documents are not operated
|
|
|
+ if (fs.existsSync(unzippedReposPath)) await fs.promises.rm(unzippedReposPath, { recursive: true });
|
|
|
+ if (fs.existsSync(pluginPath)) await fs.promises.rm(pluginPath, { recursive: true });
|
|
|
+ logger.error(err);
|
|
|
+ }
|
|
|
+
|
|
|
continue;
|
|
|
}
|
|
|
}
|
|
|
@@ -79,36 +98,71 @@ export class PluginService implements IPluginService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /*
|
|
|
+ * Install a plugin from URL and save it in the DB and file system.
|
|
|
+ */
|
|
|
async install(origin: GrowiPluginOrigin): Promise<string> {
|
|
|
+ const ghUrl = new URL(origin.url);
|
|
|
+ const ghPathname = ghUrl.pathname;
|
|
|
+ // TODO: Branch names can be specified.
|
|
|
+ const ghBranch = 'main';
|
|
|
+
|
|
|
+ const match = ghPathname.match(githubReposIdPattern);
|
|
|
+ if (ghUrl.hostname !== 'github.com' || match == null) {
|
|
|
+ throw new Error('GitHub repository URL is invalid.');
|
|
|
+ }
|
|
|
+
|
|
|
+ const ghOrganizationName = match[1];
|
|
|
+ const ghReposName = match[2];
|
|
|
+ const installedPath = `${ghOrganizationName}/${ghReposName}`;
|
|
|
+
|
|
|
+ const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
|
|
|
+ const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
|
|
|
+ const unzippedPath = pluginStoringPath;
|
|
|
+ const unzippedReposPath = path.join(pluginStoringPath, `${ghReposName}-${ghBranch}`);
|
|
|
+ const temporaryReposPath = path.join(pluginStoringPath, ghReposName);
|
|
|
+ const reposStoringPath = path.join(pluginStoringPath, `${installedPath}`);
|
|
|
+
|
|
|
+
|
|
|
+ let plugins: GrowiPlugin<GrowiPluginMeta>[];
|
|
|
+
|
|
|
try {
|
|
|
- // download
|
|
|
- const ghUrl = new URL(origin.url);
|
|
|
- const ghPathname = ghUrl.pathname;
|
|
|
- // TODO: Branch names can be specified.
|
|
|
- const ghBranch = 'main';
|
|
|
-
|
|
|
- const match = ghPathname.match(githubReposIdPattern);
|
|
|
- if (ghUrl.hostname !== 'github.com' || match == null) {
|
|
|
- throw new Error('GitHub repository URL is invalid.');
|
|
|
- }
|
|
|
+ // download github repository to file system's temporary path
|
|
|
+ await this.download(requestUrl, zipFilePath);
|
|
|
+ await this.unzip(zipFilePath, unzippedPath);
|
|
|
+ fs.renameSync(unzippedReposPath, temporaryReposPath);
|
|
|
|
|
|
- const ghOrganizationName = match[1];
|
|
|
- const ghReposName = match[2];
|
|
|
- const installedPath = `${ghOrganizationName}/${ghReposName}`;
|
|
|
+ // detect plugins
|
|
|
+ plugins = await PluginService.detectPlugins(origin, ghOrganizationName, ghReposName);
|
|
|
|
|
|
- // download github repository to local file system
|
|
|
- await this.downloadPluginRepository(ghOrganizationName, ghReposName, ghBranch);
|
|
|
+ // remove the old repository from the storing path
|
|
|
+ if (fs.existsSync(reposStoringPath)) await fs.promises.rm(reposStoringPath, { recursive: true });
|
|
|
+
|
|
|
+ // move new repository from temporary path to storing path.
|
|
|
+ fs.renameSync(temporaryReposPath, reposStoringPath);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ // clean up
|
|
|
+ if (fs.existsSync(zipFilePath)) await fs.promises.rm(zipFilePath);
|
|
|
+ if (fs.existsSync(unzippedReposPath)) await fs.promises.rm(unzippedReposPath, { recursive: true });
|
|
|
+ if (fs.existsSync(temporaryReposPath)) await fs.promises.rm(temporaryReposPath, { recursive: true });
|
|
|
+ logger.error(err);
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
|
|
|
- // delete old document
|
|
|
+ try {
|
|
|
+ // delete plugin documents if these exist
|
|
|
await this.deleteOldPluginDocument(installedPath);
|
|
|
|
|
|
- // save plugin metadata
|
|
|
- const plugins = await PluginService.detectPlugins(origin, installedPath);
|
|
|
+ // save new plugins metadata
|
|
|
await this.savePluginMetaData(plugins);
|
|
|
|
|
|
return plugins[0].meta.name;
|
|
|
}
|
|
|
catch (err) {
|
|
|
+ // clean up
|
|
|
+ if (fs.existsSync(reposStoringPath)) await fs.promises.rm(reposStoringPath, { recursive: true });
|
|
|
+ await this.deleteOldPluginDocument(installedPath);
|
|
|
logger.error(err);
|
|
|
throw err;
|
|
|
}
|
|
|
@@ -119,72 +173,46 @@ export class PluginService implements IPluginService {
|
|
|
await GrowiPlugin.deleteMany({ installedPath: path });
|
|
|
}
|
|
|
|
|
|
- private async downloadPluginRepository(ghOrganizationName: string, ghReposName: string, ghBranch: string): Promise<void> {
|
|
|
-
|
|
|
- const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
|
|
|
- const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
|
|
|
- const unzippedPath = path.join(pluginStoringPath, ghOrganizationName);
|
|
|
-
|
|
|
- const downloadFile = async(requestUrl: string, filePath: string) => {
|
|
|
- return new Promise<void>((resolve, rejects) => {
|
|
|
- axios({
|
|
|
- method: 'GET',
|
|
|
- url: requestUrl,
|
|
|
- responseType: 'stream',
|
|
|
- })
|
|
|
- .then((res) => {
|
|
|
- if (res.status === 200) {
|
|
|
- const file = fs.createWriteStream(filePath);
|
|
|
- res.data.pipe(file)
|
|
|
- .on('close', () => file.close())
|
|
|
- .on('finish', () => {
|
|
|
- return resolve();
|
|
|
- });
|
|
|
- }
|
|
|
- else {
|
|
|
- rejects(res.status);
|
|
|
- }
|
|
|
- }).catch((err) => {
|
|
|
- logger.error(err);
|
|
|
- // eslint-disable-next-line prefer-promise-reject-errors
|
|
|
- rejects('Filed to download file.');
|
|
|
- });
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const unzip = async(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike) => {
|
|
|
- try {
|
|
|
- const stream = fs.createReadStream(zipFilePath);
|
|
|
- const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
|
|
|
- const deleteZipFile = (path: fs.PathLike) => fs.unlinkSync(path);
|
|
|
-
|
|
|
- await streamToPromise(unzipStream);
|
|
|
- deleteZipFile(zipFilePath);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err);
|
|
|
- throw new Error('Filed to unzip.');
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const renamePath = async(oldPath: fs.PathLike, newPath: fs.PathLike) => {
|
|
|
- try {
|
|
|
- // if repository already exists, delete old repository before rename path
|
|
|
- if (fs.existsSync(newPath)) await fs.promises.rm(newPath, { recursive: true });
|
|
|
- // rename repository
|
|
|
- fs.renameSync(oldPath, newPath);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err);
|
|
|
- throw new Error('Filed to rename path.');
|
|
|
- }
|
|
|
- };
|
|
|
+ // !! DO NOT USE WHERE NOT SSRF GUARDED !! -- 2022.12.26 ryoji-s
|
|
|
+ private async download(requestUrl: string, filePath: string): Promise<void> {
|
|
|
+ return new Promise<void>((resolve, rejects) => {
|
|
|
+ axios({
|
|
|
+ method: 'GET',
|
|
|
+ url: requestUrl,
|
|
|
+ responseType: 'stream',
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ if (res.status === 200) {
|
|
|
+ const file = fs.createWriteStream(filePath);
|
|
|
+ res.data.pipe(file)
|
|
|
+ .on('close', () => file.close())
|
|
|
+ .on('finish', () => {
|
|
|
+ return resolve();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ rejects(res.status);
|
|
|
+ }
|
|
|
+ }).catch((err) => {
|
|
|
+ logger.error(err);
|
|
|
+ // eslint-disable-next-line prefer-promise-reject-errors
|
|
|
+ rejects('Filed to download file.');
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- await downloadFile(requestUrl, zipFilePath);
|
|
|
- await unzip(zipFilePath, unzippedPath);
|
|
|
- await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
|
|
|
+ private async unzip(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike): Promise<void> {
|
|
|
+ try {
|
|
|
+ const stream = fs.createReadStream(zipFilePath);
|
|
|
+ const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
|
|
|
|
|
|
- return;
|
|
|
+ await streamToPromise(unzipStream);
|
|
|
+ await fs.promises.rm(zipFilePath);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ logger.error(err);
|
|
|
+ throw new Error('Filed to unzip.');
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private async savePluginMetaData(plugins: GrowiPlugin[]): Promise<void> {
|
|
|
@@ -192,9 +220,9 @@ export class PluginService implements IPluginService {
|
|
|
await GrowiPlugin.insertMany(plugins);
|
|
|
}
|
|
|
|
|
|
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
|
- private static async detectPlugins(origin: GrowiPluginOrigin, installedPath: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
|
|
|
- const packageJsonPath = path.resolve(pluginStoringPath, installedPath, 'package.json');
|
|
|
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, max-len
|
|
|
+ private static async detectPlugins(origin: GrowiPluginOrigin, ghOrganizationName: string, ghReposName: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
|
|
|
+ const packageJsonPath = path.resolve(pluginStoringPath, ghReposName, 'package.json');
|
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
|
|
|
|
const { growiPlugin } = packageJson;
|
|
|
@@ -211,7 +239,7 @@ export class PluginService implements IPluginService {
|
|
|
if (growiPlugin.isMonorepo && growiPlugin.packages != null) {
|
|
|
const plugins = await Promise.all(
|
|
|
growiPlugin.packages.map(async(subPackagePath) => {
|
|
|
- const subPackageInstalledPath = path.join(installedPath, subPackagePath);
|
|
|
+ const subPackageInstalledPath = path.join(ghReposName, subPackagePath);
|
|
|
return this.detectPlugins(origin, subPackageInstalledPath, packageJson);
|
|
|
}),
|
|
|
);
|
|
|
@@ -223,7 +251,7 @@ export class PluginService implements IPluginService {
|
|
|
}
|
|
|
const plugin = {
|
|
|
isEnabled: true,
|
|
|
- installedPath,
|
|
|
+ installedPath: `${ghOrganizationName}/${ghReposName}`,
|
|
|
origin,
|
|
|
meta: {
|
|
|
name: growiPlugin.name ?? packageName,
|