Jelajahi Sumber

Merge pull request #8 from hakumizuki/feat/g2g-auto-export-import

feat: G2G auto export import (without attachments info)
Haku Mizuki 3 tahun lalu
induk
melakukan
86d1cb6452

+ 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);
   }
 };
 

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

@@ -1,4 +1,4 @@
-import { Schema } from 'mongoose';
+import { Model, Schema, HydratedDocument } from 'mongoose';
 
 import { ITransferKey } from '~/interfaces/transfer-key';
 
@@ -7,7 +7,13 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 
 const logger = loggerFactory('growi:models:transfer-key');
 
-const schema = new Schema<ITransferKey>({
+interface ITransferKeyMethods {
+  findOneActiveTransferKey(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
+}
+
+type TransferKeyModel = Model<ITransferKey, any, ITransferKeyMethods>;
+
+const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
   expireAt: { type: Date, default: () => new Date(), expires: '30m' },
   value: { type: String, unique: true },
 }, {
@@ -17,4 +23,17 @@ const schema = new Schema<ITransferKey>({
   },
 });
 
+schema.statics.findOneActiveTransferKey = async function(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
+  let tk: HydratedDocument<ITransferKey, ITransferKeyMethods> | null;
+  try {
+    tk = await this.findOne({ value: transferKeyString });
+  }
+  catch (err) {
+    logger.error(err);
+    throw err;
+  }
+
+  return tk;
+};
+
 export default getOrCreateModel('TransferKey', schema);

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

@@ -0,0 +1,33 @@
+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];
+
+export class G2GTransferError extends ExtensibleCustomError {
+
+  readonly id = 'G2GTransferError';
+
+  code!: G2GTransferErrorCode;
+
+  constructor(message: string, code: G2GTransferErrorCode) {
+    super(message);
+    this.code = code;
+  }
+
+}
+
+export const isG2GTransferError = (err: any): err is G2GTransferError => {
+  if (err == null || typeof err !== 'object') {
+    return false;
+  }
+
+  if (err instanceof G2GTransferError) {
+    return true;
+  }
+
+  return err?.id === 'G2GTransferError';
+};

+ 251 - 13
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -1,12 +1,23 @@
+import path from 'path';
+
 import express, { NextFunction, Request, Router } from 'express';
 import { body } from 'express-validator';
+import multer from 'multer';
 
+import { SupportedAction } from '~/interfaces/activity';
+import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+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 { generateOverwriteParams } from './import';
 import { ApiV3Response } from './interfaces/apiv3-response';
 
 const logger = loggerFactory('growi:routes:apiv3:transfer');
@@ -14,6 +25,7 @@ const logger = loggerFactory('growi:routes:apiv3:transfer');
 const validator = {
   transfer: [
     body('transferKey').isString().withMessage('transferKey is required'),
+    body('collections').isArray().withMessage('collections is required'),
   ],
 };
 
@@ -21,11 +33,33 @@ const validator = {
  * Routes
  */
 module.exports = (crowi: Crowi): Router => {
-  const { g2gTransferService } = crowi;
-  if (g2gTransferService == null) {
-    throw Error('G2GTransferService is not set');
+  const {
+    g2gTransferPusherService, g2gTransferReceiverService, exportService, importService,
+    growiBridgeService,
+  } = crowi;
+  if (g2gTransferPusherService == null || g2gTransferReceiverService == null || exportService == null || importService == null
+    || growiBridgeService == null) {
+    throw Error('GROWI is not ready for g2g transfer');
   }
 
+  const uploads = multer({
+    storage: multer.diskStorage({
+      destination: (req, file, cb) => {
+        cb(null, importService.baseDir);
+      },
+      filename(req, file, cb) {
+        // to prevent hashing the file name. files with same name will be overwritten.
+        cb(null, file.originalname);
+      },
+    }),
+    fileFilter: (req, file, cb) => {
+      if (path.extname(file.originalname) === '.zip') {
+        return cb(null, true);
+      }
+      cb(new Error('Only ".zip" is allowed'));
+    },
+  });
+
   const isInstalled = crowi.configManager?.getConfig('crowi', 'app:installed');
 
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
@@ -57,20 +91,175 @@ 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 verifyAndExtractTransferKey = async(req: Request & { transferKey: TransferKey }, 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
+    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();
   };
 
   const router = express.Router();
+  const receiveRouter = express.Router();
+  const pushRouter = express.Router();
+
+  // Auto import
+  receiveRouter.post('/', uploads.single('file'), verifyAndExtractTransferKey, async(req: Request & { transferKey: TransferKey }, res: ApiV3Response) => {
+    const { file } = req;
+
+    const zipFile = importService.getFile(file.filename);
+    let data;
+
+    // ぶちこみ
+
+    const { collections, optionsMap } = req.body;
+
+    /*
+     * unzip, parse
+     */
+    let meta;
+    let innerFileStats;
+    try {
+      // unzip
+      await importService.unzip(zipFile);
+
+      // eslint-disable-next-line no-unused-vars
+      const { meta: parsedMeta, innerFileStats: _innerFileStats } = await growiBridgeService.parseZipFile(zipFile);
+      innerFileStats = _innerFileStats;
+      meta = parsedMeta;
+    }
+    catch (err) {
+      logger.error(err);
+      // adminEvent.emit('onErrorForImport', { message: err.message });
+      return;
+    }
+
+    /*
+     * validate with meta.json
+     */
+    try {
+      importService.validate(meta);
+    }
+    catch (err) {
+      logger.error(err);
+      // adminEvent.emit('onErrorForImport', { message: err.message });
+      return;
+    }
+
+    // generate maps of ImportSettings to import
+    const importSettingsMap = {};
+    innerFileStats.forEach(({ fileName, collectionName }) => {
+      // instanciate GrowiArchiveImportOption
+      const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
+
+      let importSettings;
+      // generate options
+      if (collectionName === 'configs') {
+        importSettings = importService.generateImportSettings('flushAndInsert');
+      }
+      else {
+        importSettings = importService.generateImportSettings('upsert');
+      }
+      importSettings.jsonFileName = fileName;
+
+      // generate overwrite params
+      importSettings.overwriteParams = generateOverwriteParams(collectionName, req, options);
 
-  router.post('/', validator.transfer, apiV3FormValidator, validateTransferKey, async(req: Request, res: ApiV3Response) => {
-    return;
+      importSettingsMap[collectionName] = importSettings;
+    });
+
+    /*
+     * import
+     */
+    try {
+      importService.import(collections, importSettingsMap);
+      const parameters = { action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED };
+      // activityEvent.emit('update', res.locals.activity._id, parameters);
+    }
+    catch (err) {
+      logger.error(err);
+      // adminEvent.emit('onErrorForImport', { message: err.message });
+    }
+
+    // ここまで
+
+    try {
+      data = await growiBridgeService.parseZipFile(zipFile);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
+    }
+
+    try {
+      // validate with meta.json
+      importService.validate(data.meta);
+
+      // const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD };
+      // activityEvent.emit('update', res.locals.activity._id, parameters);
+    }
+    catch {
+      const msg = 'the version of this growi and the growi that exported the data are not met';
+      const varidationErr = 'versions-are-not-met';
+      return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
+    }
+
+    try {
+      await g2gTransferReceiverService.receive(file.stream);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Error occurred while importing transfer data.', 'failed_to_receive'));
+    }
+
+    return res.apiv3({ message: 'Successfully started to receive transfer data.' });
+  });
+
+  receiveRouter.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) => {
+  // eslint-disable-next-line max-len
+  receiveRouter.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
@@ -86,7 +275,7 @@ module.exports = (crowi: Crowi): Router => {
     // Save TransferKey document
     let transferKeyString: string;
     try {
-      transferKeyString = await g2gTransferService.createTransferKey(appSiteUrl);
+      transferKeyString = await g2gTransferReceiverService.createTransferKey(appSiteUrl);
     }
     catch (err) {
       logger.error(err);
@@ -96,10 +285,59 @@ 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;
+  pushRouter.post('/transfer', accessTokenParser, loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
+    const { transferKey: transferKeyString, collections, optionsMap } = req.body;
+
+    // Parse transfer key
+    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);
+    }
+
+    // Ask growi info
+    // TODO: Ask progress as well
+    let fromGROWIInfo: IDataGROWIInfo;
+    try {
+      fromGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('GROWI is incompatible to transfer data.', 'growi_incompatible_to_transfer'));
+    }
+
+    // Check if can transfer
+    const canTransfer = await g2gTransferPusherService.canTransfer(fromGROWIInfo);
+    if (!canTransfer) {
+      logger.debug('Could not transfer.');
+      return res.apiv3Err(new ErrorV3('GROWI is incompatible to transfer data.', 'growi_incompatible_to_transfer'));
+    }
+
+    // Start transfer
+    try {
+      await g2gTransferPusherService.startTransfer(tk, collections, optionsMap);
+    }
+    catch (err) {
+      logger.error(err);
+
+      if (!isG2GTransferError(err)) {
+        return res.apiv3Err(new ErrorV3('Failed to transfer', 'failed_to_transfer'), 500);
+      }
+
+      return res.apiv3Err(new ErrorV3(err.message, err.code), 500);
+    }
+
+    return res.apiv3({ message: 'Successfully requested auto transfer.' });
   });
 
+  // Merge receiveRouter and pushRouter
+  router.use(receiveRouter, pushRouter);
+
   return router;
 };

+ 1 - 1
packages/app/src/server/routes/apiv3/import.js

@@ -55,7 +55,7 @@ const router = express.Router();
  * @param {object} req Request Object
  * @param {GrowiArchiveImportOption} options GrowiArchiveImportOption instance
  */
-const generateOverwriteParams = (collectionName, req, options) => {
+export const generateOverwriteParams = (collectionName, req, options) => {
   switch (collectionName) {
     case 'pages':
       return require('./overwrite-params/pages')(req, options);

+ 6 - 2
packages/app/src/server/service/export.js

@@ -231,10 +231,12 @@ class ExportService {
     // send terminate event
     this.emitTerminateEvent(addedZipFileStat);
 
+    return addedZipFileStat;
+
     // TODO: remove broken zip file
   }
 
-  async export(collections) {
+  async export(collections, shouldEmit = true) {
     if (this.currentProgressingStatus != null) {
       throw new Error('There is an exporting process running.');
     }
@@ -242,13 +244,15 @@ class ExportService {
     this.currentProgressingStatus = new ExportProgressingStatus(collections);
     await this.currentProgressingStatus.init();
 
+    let zipFileStat;
     try {
-      await this.exportCollectionsToZippedJson(collections);
+      zipFileStat = await this.exportCollectionsToZippedJson(collections, shouldEmit);
     }
     finally {
       this.currentProgressingStatus = null;
     }
 
+    return zipFileStat;
   }
 
   /**

+ 174 - 21
packages/app/src/server/service/g2g-transfer.ts

@@ -1,43 +1,83 @@
+import { createReadStream, ReadStream } from 'fs';
 import { Readable } from 'stream';
 
+// eslint-disable-next-line no-restricted-imports
+import rawAxios from 'axios';
+import FormData from 'form-data';
 import { Types as MongooseTypes } from 'mongoose';
 
 import TransferKeyModel from '~/server/models/transfer-key';
+import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
+import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
+
 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
+  userUpperLimit: number | null // Handle null as Infinity
+  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>
+  /**
+   * TODO
+   */
+  transferAttachments(): Promise<void>
   /**
    * Start transfer data between GROWIs
-   * @param {string} key Transfer key
+   * @param {TransferKey} tk TransferKey object
+   * @param {string[]} collections Collection name string array
+   * @param {any} optionsMap Options map
    */
-  startTransfer(key: string): Promise<Readable>
+  startTransfer(tk: TransferKey, collections: string[], optionsMap: any): Promise<void>
 }
 
 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 transfering is proceedable
-   * @param {IDataFromGROWIInfo} fromGROWIInfo
+   * 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}
    */
-  canTransfer(fromGROWIInfo: IDataFromGROWIInfo): Promise<boolean>
+  receive(zippedGROWIDataStream: Readable): Promise<void>
 }
 
-export class G2GTransferService implements Pusher, Receiver {
+export class G2GTransferPusherService implements Pusher {
 
   crowi: any;
 
@@ -46,18 +86,119 @@ 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
+    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);
+    }
 
-  public async startTransfer(key: string): Promise<Readable> { return new Readable() }
+    return toGROWIInfo;
+  }
 
-  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 transferAttachments(): Promise<void> { return }
+
+  public async startTransfer(tk: TransferKey, collections: string[], optionsMap: any): Promise<void> {
+    const { appUrl, key } = tk;
+
+    let zipFileStream: ReadStream;
+    try {
+      const shouldEmit = false;
+      const zipFileStat = await this.crowi.exportService.export(collections, shouldEmit);
+      const zipFilePath = zipFileStat.zipFilePath;
+
+      zipFileStream = createReadStream(zipFilePath);
+    }
+    catch (err) {
+      logger.error(err);
+      throw err;
+    }
+
+    // Send a zip file to other growi via axios
+    try {
+      // Use FormData to immitate browser's form data object
+      const form = new FormData();
+
+      const appTitle = this.crowi.appService.getAppTitle();
+      form.append('transferDataZipFile', zipFileStream, `${appTitle}-${Date.now}.growi.zip`);
+      form.append('collections', collections);
+      form.append('optionsMap', optionsMap);
+      await rawAxios.post('/_api/v3/g2g-transfer/', form, {
+        baseURL: appUrl.origin,
+        headers: {
+          ...form.getHeaders(), // This generates a unique boundary for multi part form data
+          [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
+        },
+      });
+    }
+    catch (errs) {
+      if (!Array.isArray(errs)) {
+        // TODO: socker.emit(failed_to_transfer);
+        return;
+      }
+
+      const err = errs[0];
+      logger.error(err);
+
+
+      // TODO: socker.emit(failed_to_transfer);
+      return;
+    }
+  }
+
+  private generateAxiosRequestConfig(tk: TransferKey) {
+    const { appUrl, key } = tk;
+
+    return {
+      baseURL: appUrl.origin,
+      headers: {
+        [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
+      },
+    };
+  }
+
+}
+
+export class G2GTransferReceiverService implements Receiver {
+
+  crowi: any;
+
+  constructor(crowi: any) {
+    this.crowi = crowi;
+  }
+
+  public async validateTransferKey(transferKeyString: string): Promise<void> {
+    // Parse to tk
+    // Find active tkd
+
+    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 };
+  }
 
-  /**
-   * 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 +213,29 @@ 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, value: transferKeyString });
     }
     catch (err) {
       logger.error(err);
       throw err;
     }
 
-    return transferKeyString;
+    return tkd.value;
   }
 
-  private onCompleteTransfer(): void { return }
+  public async receive(zipfile: Readable): Promise<void> {
+    // Import data
+    // Call onCompleteTransfer when finished
+
+    return;
+  }
+
+  /**
+   * Sync DB, etc.
+   * @returns {Promise<void>}
+   */
+  private async onCompleteTransfer(): Promise<void> { return }
 
 }

+ 1 - 0
packages/app/src/server/service/growi-bridge.js

@@ -120,6 +120,7 @@ class GrowiBridgeService {
     return {
       meta,
       fileName: path.basename(zipFile),
+      zipFilePath: zipFile,
       fileStat,
       innerFileStats,
     };