Просмотр исходного кода

Merge pull request #7122 from weseek/feat/update-documents-by-reinstall

feat: Plugin reinstall features
Ryoji Shimizu 3 лет назад
Родитель
Сommit
d2245ec112

+ 2 - 5
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -3,9 +3,6 @@ import React, { useCallback } from 'react';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
-import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
-// TODO: error notification (toast, loggerFactory)
-// TODO: i18n
 
 export const PluginInstallerForm = (): JSX.Element => {
   // const { t } = useTranslation('admin');
@@ -31,8 +28,8 @@ export const PluginInstallerForm = (): JSX.Element => {
       await apiv3Post('/plugins', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
     }
-    catch (err) {
-      toastError(err);
+    catch (e) {
+      toastError(e);
     }
   }, []);
 

+ 3 - 0
packages/app/src/server/crowi/index.js

@@ -702,6 +702,9 @@ Crowi.prototype.setupPluginService = async function() {
   if (this.pluginService == null) {
     this.pluginService = new PluginService(this);
   }
+  // download plugin repositories, if document exists but there is no repository
+  // TODO: Cannot download unless connected to the Internet at setup.
+  await this.pluginService.downloadNotExistPluginRepositories();
 };
 
 Crowi.prototype.setupPageService = async function() {

+ 1 - 1
packages/app/src/server/routes/apiv3/plugins.ts

@@ -12,7 +12,7 @@ module.exports = (crowi: Crowi) => {
 
   router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
     if (pluginService == null) {
-      return res.apiv3Err(400);
+      return res.apiv3Err('\'pluginService\' is not set up', 500);
     }
 
     try {

+ 97 - 35
packages/app/src/server/service/plugin.ts

@@ -27,7 +27,6 @@ const PLUGINS_STATIC_DIR = '/static/plugins'; // configured by express.static
 
 export type GrowiPluginResourceEntries = [installedPath: string, href: string][];
 
-
 function retrievePluginManifest(growiPlugin: GrowiPlugin): ViteManifest {
   const manifestPath = resolveFromRoot(path.join('tmp/plugins', growiPlugin.installedPath, 'dist/manifest.json'));
   const manifestStr: string = readFileSync(manifestPath, 'utf-8');
@@ -38,48 +37,96 @@ export interface IPluginService {
   install(origin: GrowiPluginOrigin): Promise<void>
   retrieveThemeHref(theme: string): Promise<string | undefined>
   retrieveAllPluginResourceEntries(): Promise<GrowiPluginResourceEntries>
+  downloadNotExistPluginRepositories(): Promise<void>
 }
 
 export class PluginService implements IPluginService {
 
+  async downloadNotExistPluginRepositories(): Promise<void> {
+    try {
+      // check all growi plugin documents
+      const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+      const growiPlugins = await GrowiPlugin.find({});
+      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;
+          // TODO: Branch names can be specified.
+          const ghBranch = 'main';
+          const match = ghPathname.match(githubReposIdPattern);
+          if (ghUrl.hostname !== 'github.com' || match == null) {
+            throw new Error('The GitHub Repository URL is invalid.');
+          }
+
+          const ghOrganizationName = match[1];
+          const ghReposName = match[2];
+
+          // download github repository to local file system
+          await this.downloadPluginRepository(ghOrganizationName, ghReposName, ghBranch);
+          continue;
+        }
+      }
+    }
+    catch (err) {
+      logger.error(err);
+    }
+  }
+
   async install(origin: GrowiPluginOrigin): Promise<void> {
+    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('The GitHub Repository URL is invalid.');
-    }
+      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('The GitHub Repository URL is invalid.');
+      }
 
-    const ghOrganizationName = match[1];
-    const ghReposName = match[2];
-    const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
+      const ghOrganizationName = match[1];
+      const ghReposName = match[2];
+      const installedPath = `${ghOrganizationName}/${ghReposName}`;
 
-    // download github repository to local file system
-    await this.download(requestUrl, ghOrganizationName, ghReposName, ghBranch);
+      // download github repository to local file system
+      await this.downloadPluginRepository(ghOrganizationName, ghReposName, ghBranch);
 
-    // save plugin metadata
-    const installedPath = `${ghOrganizationName}/${ghReposName}`;
-    const plugins = await PluginService.detectPlugins(origin, installedPath);
-    await this.savePluginMetaData(plugins);
+      // delete old document
+      await this.deleteOldPluginDocument(installedPath);
+
+      // save plugin metadata
+      const plugins = await PluginService.detectPlugins(origin, installedPath);
+      await this.savePluginMetaData(plugins);
+    }
+    catch (err) {
+      logger.error(err);
+      throw err;
+    }
 
     return;
   }
 
-  private async download(requestUrl: string, ghOrganizationName: string, ghReposName: string, ghBranch: string): Promise<void> {
+  private async deleteOldPluginDocument(path: string): Promise<void> {
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    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 renamePath = async(oldPath: fs.PathLike, newPath: fs.PathLike) => {
-      fs.renameSync(oldPath, newPath);
-    };
-
     const downloadFile = async(requestUrl: string, filePath: string) => {
-      return new Promise<void>((resolve, reject) => {
+      return new Promise<void>((resolve, rejects) => {
         axios({
           method: 'GET',
           url: requestUrl,
@@ -95,25 +142,41 @@ export class PluginService implements IPluginService {
                 });
             }
             else {
-              return reject(res.status);
+              rejects(res.status);
             }
-          }).catch((err) => {
-            return reject(err);
+          }).catch((e) => {
+            logger.error(e);
+            // eslint-disable-next-line prefer-promise-reject-errors
+            rejects('Filed to download file.');
           });
       });
     };
 
     const unzip = async(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike) => {
-      const stream = fs.createReadStream(zipFilePath);
-      const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
-      const deleteZipFile = (path: fs.PathLike) => fs.unlink(path, (err) => { return err });
-
       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) {
-        return 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.');
       }
     };
 
@@ -123,8 +186,7 @@ export class PluginService implements IPluginService {
       await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
     }
     catch (err) {
-      logger.error(err);
-      throw new Error(err);
+      throw err;
     }
 
     return;