jam411 3 лет назад
Родитель
Сommit
ddf80e3fbb

+ 1 - 0
packages/app/package.json

@@ -112,6 +112,7 @@
     "graceful-fs": "^4.1.11",
     "hast-util-select": "^5.0.2",
     "helmet": "^4.6.0",
+    "node-wget-js": "^1.0.1",
     "http-errors": "^2.0.0",
     "i18next-chained-backend": "^3.0.2",
     "i18next-http-backend": "^1.4.1",

+ 21 - 0
packages/app/src/components/Admin/Common/AdminInstallButtonRow.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+type Props = {
+  onClick: () => void,
+  disabled: boolean,
+
+}
+
+export const AdminInstallButtonRow = (props: Props): JSX.Element => {
+  // TODO: const { t } = useTranslation('admin');
+
+  return (
+    <div className="row my-3">
+      <div className="mx-auto">
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>Install</button>
+      </div>
+    </div>
+  );
+};

+ 1 - 1
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-export const PluginCard = () => {
+export const PluginCard = (): JSX.Element => {
   return (
     <div>
       <div>

+ 32 - 20
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -3,17 +3,34 @@ import React, { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
 
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
 // TODO: error notification (toast, loggerFactory)
 // TODO: i18n
 
 export const PluginInstallerForm = (): JSX.Element => {
   const { t } = useTranslation('admin');
 
-  const submitHandler = useCallback(async() => {
+  const submitHandler = useCallback(async(e) => {
+    e.preventDefault();
+
+    const formData = e.target.elements;
+
+    const {
+      'pluginInstallerForm[url]': { value: url },
+      'pluginInstallerForm[ghBranch]': { value: ghBranch },
+      'pluginInstallerForm[ghTag]': { value: ghTag },
+    } = formData;
+
+    const pluginInstallerForm = {
+      url,
+      ghBranch,
+      ghTag,
+    };
+
     try {
-      // await adminAppContainer.updateAppSettingHandler();
+      await apiv3Post('/plugins-extention', { pluginInstallerForm });
       toastSuccess(t('toaster.update_successed', { target: t('app_settings') }));
     }
     catch (err) {
@@ -23,19 +40,17 @@ export const PluginInstallerForm = (): JSX.Element => {
   }, [t]);
 
   return (
-    <>
+    <form role="form" onSubmit={submitHandler}>
       <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">Plugin URL</label>
-        {/* <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:plugins_extention.plugin_url')}</label> */}
+        <label className="text-left text-md-right col-md-3 col-form-label">GitHub repository URL</label>
         <div className="col-md-6">
           <input
             className="form-control"
             type="text"
             // defaultValue={adminAppContainer.state.title || ''}
-            // onChange={(e) => {
-            //   adminAppContainer.changeTitle(e.target.value);
-            // }}
+            name="pluginInstallerForm[url]"
             placeholder="https://github.com/weseek/growi-plugins/vibrant-dark-ui"
+            required
           />
           <p className="form-text text-muted">Install the plugin in GROWI: Enter the URL of the plugin repository and press the Update.</p>
           {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
@@ -47,10 +62,7 @@ export const PluginInstallerForm = (): JSX.Element => {
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
-            // onChange={(e) => {
-            //   adminAppContainer.changeTitle(e.target.value);
-            // }}
+            name="pluginInstallerForm[ghBranch]"
             placeholder="main"
           />
           <p className="form-text text-muted">branch name</p>
@@ -62,18 +74,18 @@ export const PluginInstallerForm = (): JSX.Element => {
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
-            // onChange={(e) => {
-            //   adminAppContainer.changeTitle(e.target.value);
-            // }}
+            name="pluginInstallerForm[ghTag]"
             placeholder="tags"
           />
           <p className="form-text text-muted">tag name</p>
         </div>
       </div>
 
-      <AdminUpdateButtonRow onClick={submitHandler} disabled={false}/>
-      {/* <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} /> */}
-    </>
+      <div className="row my-3">
+        <div className="mx-auto">
+          <button type="submit" className="btn btn-primary" >Install</button>
+        </div>
+      </div>
+    </form>
   );
 };

+ 2 - 1
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -64,7 +64,8 @@ const AdminLayout = dynamic(() => import('../../components/Layout/AdminLayout'),
 // named export
 const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage').then(mod => mod.UserGroupPage), { ssr: false });
 const AuditLogManagement = dynamic(() => import('../../components/Admin/AuditLogManagement').then(mod => mod.AuditLogManagement), { ssr: false });
-const PluginsExtensionPageContents = dynamic(() => import('../../components/Admin/PluginsExtension/PluginsExtensionPageContents').then(mod => mod.PluginsExtensionPageContents), { ssr: false });
+const PluginsExtensionPageContents = dynamic(() => import('../../components/Admin/PluginsExtension/PluginsExtensionPageContents')
+  .then(mod => mod.PluginsExtensionPageContents), { ssr: false });
 
 const pluginUtils = new PluginUtils();
 

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

@@ -26,6 +26,7 @@ import { InstallerService } from '../service/installer';
 import PageService from '../service/page';
 import PageGrantService from '../service/page-grant';
 import PageOperationService from '../service/page-operation';
+import { PluginService } from '../service/plugin';
 import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
 import { UserNotificationService } from '../service/user-notification';
@@ -66,6 +67,7 @@ function Crowi() {
   this.exportService = null;
   this.importService = null;
   this.searchService = null;
+  this.pluginService = null;
   this.socketIoService = null;
   this.pageService = null;
   this.syncPageStatusService = null;
@@ -119,6 +121,7 @@ Crowi.prototype.init = async function() {
     this.scanRuntimeVersions(),
     this.setupPassport(),
     this.setupSearcher(),
+    this.setupPluginer(),
     this.setupMailer(),
     this.setupSlackIntegrationService(),
     this.setUpFileUpload(),
@@ -368,6 +371,13 @@ Crowi.prototype.setupSearcher = async function() {
   this.searchService = new SearchService(this);
 };
 
+/**
+ * setup PluginService
+ */
+Crowi.prototype.setupPluginer = async function() {
+  this.pluginService = new PluginService(this);
+};
+
 Crowi.prototype.setupMailer = async function() {
   const MailService = require('~/server/service/mail');
   this.mailService = new MailService(this);

+ 2 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -99,6 +99,8 @@ module.exports = (crowi, app, isInstalled) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
+  router.use('/plugins-extention', require('./plugins-extention')(crowi));
+
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
 

+ 31 - 0
packages/app/src/server/routes/apiv3/plugins-extention.ts

@@ -0,0 +1,31 @@
+import express, { Request } from 'express';
+
+import Crowi from '../../crowi';
+import { PluginService } from '../../service/plugin';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+
+type PluginInstallerFormRequest = Request & { form: any };
+
+module.exports = (crowi: Crowi) => {
+  const router = express.Router();
+  // const { pluginService } = crowi;
+
+  router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
+    if (PluginService == null) {
+      return res.apiv3Err(400);
+    }
+
+    try {
+      PluginService.install(crowi, req.body.pluginInstallerForm);
+      return res.apiv3(200);
+    }
+    catch (err) {
+      // TODO: error handling
+      return res.apiv3Err(err, 400);
+    }
+  });
+
+  return router;
+};

+ 40 - 4
packages/app/src/server/service/plugin.ts

@@ -1,19 +1,55 @@
-import fs from 'fs';
+
 import path from 'path';
 
+import wget from 'node-wget-js';
+
+
 import { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
-
 const pluginStoringPath = resolveFromRoot('tmp/plugins');
 
+async function downloadURL(urls: string, filename: string) {
+  await wget({ url: urls, dest: filename });
+}
+
 export class PluginService {
 
-  static async install(origin: GrowiPluginOrigin): Promise<void> {
-    // TODO: download
+  static async install(crowi, origin: GrowiPluginOrigin): Promise<void> {
+    const { importService } = crowi;
+    // download
+    const ghUrl = origin.url;
+    const ghBranch = origin.ghBranch;
+    const ghTag = origin.ghTag;
+    const downloadDir = path.join(process.cwd(), 'tmp/plugins/');
+    downloadURL(`${ghUrl}/archive/refs/heads/master.zip`, downloadDir);
+    const test = '/workspace/growi/packages/app/tmp/plugins/master.zip';
+    // try {
+    //   await downloadURL(`${ghUrl}/archive/refs/heads/master.zip`, downloadDir);
+    // }
+    // catch (err) {
+    //   // TODO:
+    // }
+
+    // console.log(`${downloadDir}master.zip`);
+
+    // // unzip
+    // const files = await unzip(`${downloadDir}master.zip`);
+    // console.log('fle', files);
+    // const file = await importService.unzip(`${downloadDir}master.zip`);
+    // console.log(file);
+    // try {
+    //   // unzip
+    //   const file = await importService.unzip(zipFile);
+    //   console.log('fle', file)
+    // }
+    // catch (err) {
+    //   // TODO:
+    // }
+
     // TODO: detect plugins
     // TODO: save documents
     return;

+ 7 - 0
yarn.lock

@@ -16364,6 +16364,13 @@ node-releases@^2.0.6:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
   integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
 
+node-wget-js@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/node-wget-js/-/node-wget-js-1.0.1.tgz#2390bf9c9f99f280cc7a221d07d096103161e78c"
+  integrity sha512-SXzjefvZvJc5kn9kqsZhs0es8aQ1o9pnnIpzA6CPeHb7CaIfl+7OkO1n8uqyVawMzzUfhEXxW6vbqUsWEgSaFw==
+  dependencies:
+    request "^2.88.0"
+
 nodemailer-ses-transport@~1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.1.tgz#dc0598c1bf53e8652e632e8f31692ce022d7dea9"