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

/_api/v3/g2g-transfer/ & /_api/v3/g2g-transfer/transfer/ WIP

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

+ 99 - 9
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -1,13 +1,18 @@
+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 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');
 
@@ -21,9 +26,9 @@ const validator = {
  * Routes
  */
 module.exports = (crowi: Crowi): Router => {
-  const { g2gTransferService } = crowi;
-  if (g2gTransferService == null) {
-    throw Error('G2GTransferService is not set');
+  const { g2gTransferService, exportService } = crowi;
+  if (g2gTransferService == null || exportService == null) {
+    throw Error('GROWI is not ready for g2g transfer');
   }
 
   const isInstalled = crowi.configManager?.getConfig('crowi', 'app:installed');
@@ -57,16 +62,41 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3Err(new ErrorV3('Body param "appSiteUrl" is required when GROWI is NOT installed yet'), 400);
   };
 
-  // Middleware to check if key is valid or not
-  const validateTransferKey = async(req: Request, res: ApiV3Response, next: NextFunction) => {
-    const { transferKey } = req.body;
-    // TODO: Check key
+  // Local middleware to check if key is valid or not
+  const verifyAndExtractTransferKeyForImport = async(req: Request & { transferKey: any }, res: ApiV3Response, next: NextFunction) => {
+    const transferKeyString = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME];
+
+    if (typeof transferKeyString !== 'string') {
+      return res.apiv3Err(new ErrorV3('Invalid transfer key or not set.', 'invalid_transfer_key'), 400);
+    }
+
+    let transferKey;
+    try {
+      transferKey = await (TransferKeyModel as any).findOneActiveTransferKey(transferKeyString); // TODO: Improve TS of models
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Error occurred while trying to fing a transfer key.', 'failed_to_find_transfer_key'), 500);
+    }
+
+    if (transferKey == null) {
+      return res.apiv3Err(new ErrorV3('Transfer key has expired or not found.', 'transfer_key_expired_or_not_found'), 404);
+    }
+
+    // Inject transferKey to req
+    req.transferKey = transferKey;
+
     next();
   };
 
   const router = express.Router();
 
-  router.post('/', validator.transfer, apiV3FormValidator, validateTransferKey, async(req: Request, res: ApiV3Response) => {
+  // Auto import
+  router.post('/', verifyAndExtractTransferKeyForImport, async(req: Request & { transferKey: any }, res: ApiV3Response) => {
+    const { transferKey } = req;
+
+
+
     return;
   });
 
@@ -96,9 +126,69 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3({ transferKey: transferKeyString });
   });
 
+  // Auto export
+  // TODO: Use socket to send progress info to the client
   // eslint-disable-next-line max-len
   router.post('/transfer', accessTokenParser, loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
-    return;
+    // 1. Ask
+    // 2. Start
+
+    const { transferKey: transferKeyString } = req.body;
+
+    let tk: TransferKey;
+    try {
+      tk = TransferKey.parse(transferKeyString);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
+    }
+
+    const canTransfer = await g2gTransferService.canTransfer();
+
+    const { appUrl, key } = tk;
+
+    // Generate export zip
+    let zipFile;
+    try {
+      zipFile = await g2gTransferService.startTransfer(tk);
+    }
+    catch (err) {
+
+    }
+    // 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;
+      }
+    })();
+
+    return res.apiv3({ message: 'Successfully requested auto transfer.' });
   });
 
   return router;

+ 89 - 17
packages/app/src/server/service/g2g-transfer.ts

@@ -5,18 +5,32 @@ import { Types as MongooseTypes } from 'mongoose';
 import TransferKeyModel from '~/server/models/transfer-key';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
+import axios from 'axios';
 
 const logger = loggerFactory('growi:service:g2g-transfer');
 
+export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
+
 /**
  * Data used for comparing to/from GROWI information
  */
-export type IDataFromGROWIInfo = {
+export type IDataGROWIInfo = {
   version: string
   userLimit: number
+  attachmentInfo: any
 }
 
 interface Pusher {
+  /**
+   * Send to-growi a request to get growi info
+   * @param {TransferKey} tk Transfer key
+   */
+  askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo>
+  /**
+   * Check if transfering is proceedable
+   * @param {IDataGROWIInfo} fromGROWIInfo
+   */
+   canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean>
   /**
    * Start transfer data between GROWIs
    * @param {string} key Transfer key
@@ -31,13 +45,21 @@ interface Receiver {
    */
   validateTransferKey(key: string): Promise<boolean>
   /**
-   * Check if transfering is proceedable
-   * @param {IDataFromGROWIInfo} fromGROWIInfo
+   * 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>)
    */
-  canTransfer(fromGROWIInfo: IDataFromGROWIInfo): Promise<boolean>
+   createTransferKey(appSiteUrl: URL): Promise<string>
+   /**
+    * Receive transfer request and import data.
+    * @param {Readable} zippedGROWIDataStream
+    * @returns {void}
+    */
+   receive(zippedGROWIDataStream: Readable): Promise<void>
 }
 
-export class G2GTransferService implements Pusher, Receiver {
+export class G2GTransferPusherService implements Pusher {
 
   crowi: any;
 
@@ -46,18 +68,52 @@ export class G2GTransferService implements Pusher, Receiver {
     this.crowi = crowi;
   }
 
-  public async canTransfer(fromGROWIInfo: IDataFromGROWIInfo): Promise<boolean> { return true }
+  public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
+    // axios get
+    // return IDataGROWIInfo
 
-  public async startTransfer(key: string): Promise<Readable> { return new Readable() }
+    return { userLimit: 100, version: '6.0.0', attachmentInfo: {} };
+  }
 
-  public async validateTransferKey(key: string): Promise<boolean> { return true }
+  public async canTransfer(fromGROWIInfo: IDataGROWIInfo): Promise<boolean> {
+    // Check if Transfer key is alive
+    // Ask toGROWI about toGROWIInfo
+    // Compare GROWIInfos
+
+    return false;
+  }
+
+  public async startTransfer(transferKeyString: string): Promise<any> {
+    let tk: TransferKey;
+    try {
+      tk = TransferKey.parse(transferKeyString);
+    }
+    catch (err) {
+      logger.error(err);
+      return Error('');
+    }
+
+    const { appUrl, key } = tk;
+
+    await axios.post('/_api/v3/g2g-transfer/', { whatItShouldBe: 'a zip file' }, {
+      baseURL: appUrl.origin,
+      headers: {
+        [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
+      },
+    });
+  }
+
+}
+
+export class G2GTransferReceiverService implements Receiver {
+
+  public async validateTransferKey(transferKeyString: string): Promise<boolean> {
+    // Parse to tk
+    // Find active tkd
+
+    return true;
+  }
 
-  /**
-   * 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>)
-   */
   public async createTransferKey(appSiteUrl: URL): Promise<string> {
     const uuid = new MongooseTypes.ObjectId().toString();
 
@@ -72,17 +128,33 @@ export class G2GTransferService implements Pusher, Receiver {
     }
 
     // Save TransferKey document
+    let tkd;
     try {
-      await TransferKeyModel.create({ _id: uuid, appSiteUrl, value: transferKeyString });
+      tkd = await TransferKeyModel.create({ _id: uuid, appSiteUrl, value: transferKeyString });
     }
     catch (err) {
       logger.error(err);
       throw err;
     }
 
-    return transferKeyString;
+    return tkd.value;
   }
 
-  private onCompleteTransfer(): void { return }
+  public async receive(zippedGROWIDataStream: Readable): Promise<void> {
+    // FIRST, Save from growi data into config
+    // Maybe save status there as well (completed)
+    // Receive a zip file via stream
+    // Unzip
+    // Import data
+    // Call onCompleteTransfer when finished
+
+    return;
+  }
+
+  /**
+   * Sync DB, etc.
+   * @returns {Promise<void>}
+   */
+  private async onCompleteTransfer(): Promise<void> { return }
 
 }