Taichi Masuyama 3 лет назад
Родитель
Сommit
9ede9f55ec

+ 8 - 4
packages/app/src/server/crowi/index.js

@@ -22,7 +22,7 @@ import AclService from '../service/acl';
 import AppService from '../service/app';
 import AttachmentService from '../service/attachment';
 import ConfigManager from '../service/config-manager';
-import { G2GTransferService } from '../service/g2g-transfer';
+import { G2GTransferPusherService, G2GTransferReceiverService } from '../service/g2g-transfer';
 import { InstallerService } from '../service/installer';
 import PageService from '../service/page';
 import PageGrantService from '../service/page-grant';
@@ -54,7 +54,8 @@ function Crowi() {
   this.config = {};
   this.configManager = null;
   this.s2sMessagingService = null;
-  this.g2gTransferService = null;
+  this.g2gTransferPusherService = null;
+  this.g2gTransferReceiverService = null;
   this.mailService = null;
   this.passportService = null;
   this.globalNotificationService = null;
@@ -745,8 +746,11 @@ Crowi.prototype.setupSlackIntegrationService = async function() {
 };
 
 Crowi.prototype.setupG2GTransferService = async function() {
-  if (this.g2gTransferService == null) {
-    this.g2gTransferService = new G2GTransferService(this);
+  if (this.g2gTransferPusherService == null) {
+    this.g2gTransferPusherService = new G2GTransferPusherService(this);
+  }
+  if (this.g2gTransferReceiverService == null) {
+    this.g2gTransferReceiverService = new G2GTransferReceiverService(this);
   }
 };
 

+ 1 - 1
packages/app/src/server/models/transfer-key.ts

@@ -11,7 +11,7 @@ interface ITransferKeyMethods {
   findOneActiveTransferKey(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
 }
 
-type TransferKeyModel = Model<ITransferKey, {}, ITransferKeyMethods>;
+type TransferKeyModel = Model<ITransferKey, any, ITransferKeyMethods>;
 
 const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
   expireAt: { type: Date, default: () => new Date(), expires: '30m' },

+ 1 - 0
packages/app/src/server/models/vo/g2g-transfer-error.ts

@@ -2,6 +2,7 @@ import ExtensibleCustomError from 'extensible-custom-error';
 
 export const G2GTransferErrorCode = {
   INVALID_TRANSFER_KEY_STRING: 'INVALID_TRANSFER_KEY_STRING',
+  FAILED_TO_RETREIVE_GROWI_INFO: 'FAILED_TO_RETREIVE_GROWI_INFO',
 } as const;
 
 export type G2GTransferErrorCode = typeof G2GTransferErrorCode[keyof typeof G2GTransferErrorCode];

+ 70 - 46
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -1,18 +1,17 @@
-import axios from 'axios';
 import express, { NextFunction, Request, Router } from 'express';
 import { body } from 'express-validator';
 
-import loggerFactory from '~/utils/logger';
 import TransferKeyModel from '~/server/models/transfer-key';
+import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
+import { IDataGROWIInfo, X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
+import loggerFactory from '~/utils/logger';
+import { TransferKey } from '~/utils/vo/transfer-key';
 
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import ErrorV3 from '../../models/vo/error-apiv3';
 
 import { ApiV3Response } from './interfaces/apiv3-response';
-import { TransferKey } from '~/utils/vo/transfer-key';
-import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
-import { X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
 
 const logger = loggerFactory('growi:routes:apiv3:transfer');
 
@@ -26,8 +25,8 @@ const validator = {
  * Routes
  */
 module.exports = (crowi: Crowi): Router => {
-  const { g2gTransferService, exportService } = crowi;
-  if (g2gTransferService == null || exportService == null) {
+  const { g2gTransferPusherService, g2gTransferReceiverService, exportService } = crowi;
+  if (g2gTransferPusherService == null || g2gTransferReceiverService == null || exportService == null) {
     throw Error('GROWI is not ready for g2g transfer');
   }
 
@@ -63,7 +62,7 @@ module.exports = (crowi: Crowi): Router => {
   };
 
   // Local middleware to check if key is valid or not
-  const verifyAndExtractTransferKeyForImport = async(req: Request & { transferKey: any }, res: ApiV3Response, next: NextFunction) => {
+  const verifyAndExtractTransferKey = async(req: Request & { transferKey: TransferKey }, res: ApiV3Response, next: NextFunction) => {
     const transferKeyString = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME];
 
     if (typeof transferKeyString !== 'string') {
@@ -84,7 +83,13 @@ module.exports = (crowi: Crowi): Router => {
     }
 
     // Inject transferKey to req
-    req.transferKey = transferKey;
+    try {
+      req.transferKey = TransferKey.parse(transferKey.value);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Transfer key is invalid.', 'invalid_transfer_key'), 500);
+    }
 
     next();
   };
@@ -92,15 +97,32 @@ module.exports = (crowi: Crowi): Router => {
   const router = express.Router();
 
   // Auto import
-  router.post('/', verifyAndExtractTransferKeyForImport, async(req: Request & { transferKey: any }, res: ApiV3Response) => {
+  router.post('/', verifyAndExtractTransferKey, async(req: Request & { transferKey: TransferKey }, res: ApiV3Response) => {
     const { transferKey } = req;
 
 
-
     return;
   });
 
-  router.post('/generate-key', /* accessTokenParser, adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, */ async(req: Request, res: ApiV3Response) => {
+  router.get('/growi-info', /* verifyAndExtractTransferKey, */ async(req: Request & { transferKey: TransferKey }, res: ApiV3Response) => {
+    let growiInfo: IDataGROWIInfo;
+    try {
+      growiInfo = await g2gTransferReceiverService.answerGROWIInfo();
+    }
+    catch (err) {
+      logger.error(err);
+
+      if (!isG2GTransferError(err)) {
+        return res.apiv3Err(new ErrorV3('Failed to prepare growi info', 'failed_to_prepare_growi_info'), 500);
+      }
+
+      return res.apiv3Err(new ErrorV3(err.message, err.code), 500);
+    }
+
+    return res.apiv3({ growiInfo });
+  });
+
+  router.post('/generate-key', accessTokenParser, adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => {
     const strAppSiteUrl = req.body.appSiteUrl ?? crowi.configManager?.getConfig('crowi', 'app:siteUrl');
 
     // Generate transfer key string
@@ -144,49 +166,51 @@ module.exports = (crowi: Crowi): Router => {
       return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
     }
 
-    const canTransfer = await g2gTransferService.canTransfer();
+    const canTransfer = await g2gTransferPusherService.canTransfer();
 
     const { appUrl, key } = tk;
 
     // Generate export zip
-    let zipFile;
+    let zipFileStream;
     try {
-      zipFile = await g2gTransferService.startTransfer(tk);
+      zipFileStream = await g2gTransferPusherService.startTransfer(tk);
     }
     catch (err) {
-
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Error occurred while'));
     }
-    // Send a zip file to other growi via axios
-
-    (async() => {
-      try {
-        await axios.post('/_api/v3/g2g-transfer/', {}, {
-          baseURL: appUrl.origin,
-          headers: {
-            [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
-          },
-        });
-      }
-      catch (errs) {
-        if (!Array.isArray(errs)) {
-          // TODO: socker.emit(failed_to_transfer);
-          return;
-        }
-
-        const err = errs[0];
 
-        if (!isG2GTransferError(err)) {
-          // TODO: socker.emit(failed_to_transfer);
-          return;
-        }
-
-        const g2gTransferError = err;
-
-        logger.error(g2gTransferError);
-        // TODO: socker.emit(failed_to_transfer);
-        return;
-      }
-    })();
+    // Send a zip file to other growi via axios
+    // (async() => {
+    //   try {
+    //     // TODO: Make zipFileStream work
+    //     await axios.post('/_api/v3/g2g-transfer/', zipFileStream, {
+    //       baseURL: appUrl.origin,
+    //       headers: {
+    //         [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
+    //       },
+    //     });
+    //   }
+    //   catch (errs) {
+    //     if (!Array.isArray(errs)) {
+    //       // TODO: socker.emit(failed_to_transfer);
+    //       return;
+    //     }
+
+    //     const err = errs[0];
+
+    //     if (!isG2GTransferError(err)) {
+    //       // TODO: socker.emit(failed_to_transfer);
+    //       return;
+    //     }
+
+    //     const g2gTransferError = err;
+
+    //     logger.error(g2gTransferError);
+    //     // TODO: socker.emit(failed_to_transfer);
+    //     return;
+    //   }
+    // })();
 
     return res.apiv3({ message: 'Successfully requested auto transfer.' });
   });

+ 64 - 29
packages/app/src/server/service/g2g-transfer.ts

@@ -3,9 +3,11 @@ import { Readable } from 'stream';
 import { Types as MongooseTypes } from 'mongoose';
 
 import TransferKeyModel from '~/server/models/transfer-key';
+import axios, { customAxiosXTar } from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
-import axios from 'axios';
+
+import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
 
 const logger = loggerFactory('growi:service:g2g-transfer');
 
@@ -16,7 +18,7 @@ export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
  */
 export type IDataGROWIInfo = {
   version: string
-  userLimit: number
+  userUpperLimit: number | null // Handle null as Infinity
   attachmentInfo: any
 }
 
@@ -30,33 +32,43 @@ interface Pusher {
    * Check if transfering is proceedable
    * @param {IDataGROWIInfo} fromGROWIInfo
    */
-   canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean>
+  canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean>
+  /**
+   * TODO
+   */
+  transferAttachments(): Promise<void>
   /**
    * Start transfer data between GROWIs
-   * @param {string} key Transfer key
+   * @param {TransferKey} tk TransferKey object
    */
-  startTransfer(key: string): Promise<Readable>
+  startTransfer(tk: TransferKey): Promise<Readable>
 }
 
 interface Receiver {
   /**
    * Check if key is not expired
+   * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
    * @param {string} key Transfer key
    */
-  validateTransferKey(key: string): Promise<boolean>
+  validateTransferKey(key: string): Promise<void>
+  /**
+   * Check if key is not expired
+   * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
+   */
+  answerGROWIInfo(): Promise<IDataGROWIInfo>
   /**
    * This method receives appSiteUrl to create a TransferKey document and returns generated transfer key string.
    * UUID is the same value as the created document's _id.
    * @param {URL} appSiteUrl URL type appSiteUrl
    * @returns {string} Transfer key string (e.g. http://localhost:3000__grw_internal_tranferkey__<uuid>)
    */
-   createTransferKey(appSiteUrl: URL): Promise<string>
-   /**
-    * Receive transfer request and import data.
-    * @param {Readable} zippedGROWIDataStream
-    * @returns {void}
-    */
-   receive(zippedGROWIDataStream: Readable): Promise<void>
+  createTransferKey(appSiteUrl: URL): Promise<string>
+  /**
+   * Receive transfer request and import data.
+   * @param {Readable} zippedGROWIDataStream
+   * @returns {void}
+   */
+  receive(zippedGROWIDataStream: Readable): Promise<void>
 }
 
 export class G2GTransferPusherService implements Pusher {
@@ -70,9 +82,21 @@ export class G2GTransferPusherService implements Pusher {
 
   public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
     // axios get
-    // return IDataGROWIInfo
+    let toGROWIInfo: IDataGROWIInfo;
+    try {
+      const res = await axios.get('/_api/v3/g2g-transfer/growi-info', this.generateAxiosRequestConfig(tk));
+      toGROWIInfo = {
+        userUpperLimit: res.data.userUpperLimit,
+        version: res.data.version,
+        attachmentInfo: res.data.attachmentInfo,
+      };
+    }
+    catch (err) {
+      logger.error(err);
+      throw new G2GTransferError('Failed to retreive growi info.', G2GTransferErrorCode.FAILED_TO_RETREIVE_GROWI_INFO);
+    }
 
-    return { userLimit: 100, version: '6.0.0', attachmentInfo: {} };
+    return toGROWIInfo;
   }
 
   public async canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean> {
@@ -83,35 +107,46 @@ export class G2GTransferPusherService implements Pusher {
     return false;
   }
 
-  public async startTransfer(transferKeyString: string): Promise<any> {
-    let tk: TransferKey;
-    try {
-      tk = TransferKey.parse(transferKeyString);
-    }
-    catch (err) {
-      logger.error(err);
-      return Error('');
-    }
+  public async transferAttachments(): Promise<void> { return }
+
+  public async startTransfer(tk: TransferKey): Promise<any> {
+    await customAxiosXTar.post('/_api/v3/g2g-transfer/', { whatItShouldBe: 'a zip file' }, this.generateAxiosRequestConfig(tk));
+  }
 
+  private generateAxiosRequestConfig(tk: TransferKey) {
     const { appUrl, key } = tk;
 
-    await axios.post('/_api/v3/g2g-transfer/', { whatItShouldBe: 'a zip file' }, {
+    return {
       baseURL: appUrl.origin,
       headers: {
         [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
       },
-    });
+    };
   }
 
 }
 
 export class G2GTransferReceiverService implements Receiver {
 
-  public async validateTransferKey(transferKeyString: string): Promise<boolean> {
+  crowi: any;
+
+  constructor(crowi: any) {
+    this.crowi = crowi;
+  }
+
+  public async validateTransferKey(transferKeyString: string): Promise<void> {
     // Parse to tk
     // Find active tkd
 
-    return true;
+    return;
+  }
+
+  public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
+    const userUpperLimit = this.crowi.configManager.getConfig('crowi', 'security:userUpperLimit');
+    const version = this.crowi.version;
+    const attachmentInfo = {}; // TODO: Impl
+
+    return { userUpperLimit, version, attachmentInfo };
   }
 
   public async createTransferKey(appSiteUrl: URL): Promise<string> {
@@ -130,7 +165,7 @@ export class G2GTransferReceiverService implements Receiver {
     // Save TransferKey document
     let tkd;
     try {
-      tkd = await TransferKeyModel.create({ _id: uuid, appSiteUrl, value: transferKeyString });
+      tkd = await TransferKeyModel.create({ _id: uuid, value: transferKeyString });
     }
     catch (err) {
       logger.error(err);