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

Merge pull request #7109 from mizozobu/imprv/mv-logics

imprv: mv logics
Syunsuke Komma 3 лет назад
Родитель
Сommit
c723b05e9b

+ 1 - 4
packages/app/src/components/Admin/G2GDataTransfer.tsx

@@ -45,10 +45,7 @@ const G2GDataTransfer = (): JSX.Element => {
   }, []);
   }, []);
 
 
   const setCollectionsAndSelectedCollections = useCallback(async() => {
   const setCollectionsAndSelectedCollections = useCallback(async() => {
-    const [{ data: collectionsData }, { data: statusData }] = await Promise.all([
-      apiv3Get<{collections: any[]}>('/mongo/collections', {}),
-      apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
-    ]);
+    const { data: collectionsData } = await apiv3Get<{collections: any[]}>('/mongo/collections', {});
 
 
     // filter only not ignored collection names
     // filter only not ignored collection names
     const filteredCollections = collectionsData.collections.filter((collectionName) => {
     const filteredCollections = collectionsData.collections.filter((collectionName) => {

+ 26 - 77
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -6,9 +6,8 @@ import express, { NextFunction, Request, Router } from 'express';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 import multer from 'multer';
 import multer from 'multer';
 
 
-import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
-import { IDataGROWIInfo, uploadConfigKeys, X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
+import { IDataGROWIInfo, X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
 
@@ -16,7 +15,6 @@ import { TransferKey } from '~/utils/vo/transfer-key';
 import Crowi from '../../crowi';
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 
-import { generateOverwriteParams } from './import';
 import { ApiV3Response } from './interfaces/apiv3-response';
 import { ApiV3Response } from './interfaces/apiv3-response';
 
 
 interface AuthorizedRequest extends Request {
 interface AuthorizedRequest extends Request {
@@ -135,9 +133,6 @@ module.exports = (crowi: Crowi): Router => {
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   receiveRouter.post('/', uploads.single('transferDataZipFile'), validateTransferKey, async(req: Request, res: ApiV3Response) => {
   receiveRouter.post('/', uploads.single('transferDataZipFile'), validateTransferKey, async(req: Request, res: ApiV3Response) => {
     const { file } = req;
     const { file } = req;
-
-    const zipFile = importService.getFile(file.filename);
-
     const {
     const {
       collections: strCollections,
       collections: strCollections,
       optionsMap: strOptionsMap,
       optionsMap: strOptionsMap,
@@ -145,7 +140,9 @@ module.exports = (crowi: Crowi): Router => {
       uploadConfigs: strUploadConfigs,
       uploadConfigs: strUploadConfigs,
     } = req.body;
     } = req.body;
 
 
-    // Parse multipart form data
+    /*
+     * parse multipart form data
+     */
     let collections;
     let collections;
     let optionsMap;
     let optionsMap;
     let sourceGROWIUploadConfigs;
     let sourceGROWIUploadConfigs;
@@ -156,19 +153,18 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to parse body.', 'parse_failed'), 500);
+      return res.apiv3Err(new ErrorV3('Failed to parse request body.', 'parse_failed'), 500);
     }
     }
 
 
     /*
     /*
-     * unzip, parse
+     * unzip and parse
      */
      */
     let meta;
     let meta;
     let innerFileStats;
     let innerFileStats;
     try {
     try {
-      // unzip
+      const zipFile = importService.getFile(file.filename);
       await importService.unzip(zipFile);
       await importService.unzip(zipFile);
 
 
-      // eslint-disable-next-line no-unused-vars
       const { meta: parsedMeta, innerFileStats: _innerFileStats } = await growiBridgeService.parseZipFile(zipFile);
       const { meta: parsedMeta, innerFileStats: _innerFileStats } = await growiBridgeService.parseZipFile(zipFile);
       innerFileStats = _innerFileStats;
       innerFileStats = _innerFileStats;
       meta = parsedMeta;
       meta = parsedMeta;
@@ -178,85 +174,41 @@ module.exports = (crowi: Crowi): Router => {
       return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
       return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
     }
     }
 
 
+    /*
+     * validate meta.json
+     */
     try {
     try {
-      // validate with meta.json
       importService.validate(meta);
       importService.validate(meta);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-
-      const msg = 'the version of this growi and the growi that exported the data are not met';
-      const varidationErr = 'version_incompatible';
-      return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
+      return res.apiv3Err(
+        new ErrorV3(
+          'the version of this growi and the growi that exported the data are not met',
+          'version_incompatible',
+        ),
+        500,
+      );
     }
     }
 
 
-    // generate maps of ImportSettings to import
-    const importSettingsMap = {};
+    /*
+     * generate maps of ImportSettings to import
+     */
+    let importSettingsMap;
     try {
     try {
-      innerFileStats.forEach(({ fileName, collectionName }) => {
-        // instanciate GrowiArchiveImportOption
-        const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
-
-        // generate options
-        if (collectionName === 'configs' && options.mode !== 'flushAndInsert') {
-          throw Error('`flushAndInsert` is only available as an import setting for configs collection');
-        }
-        if (collectionName === 'pages' && options.mode === 'insert') {
-          throw Error('`insert` is not available as an import setting for pages collection');
-        }
-        if (collectionName === 'attachmentFiles.chunks') {
-          throw Error('`attachmentFiles.chunks` must not be transferred. Please omit it from request body `collections`.');
-        }
-        if (collectionName === 'attachmentFiles.files') {
-          throw Error('`attachmentFiles.files` must not be transferred. Please omit it from request body `collections`.');
-        }
-
-        const importSettings = importService.generateImportSettings(options.mode);
-
-        importSettings.jsonFileName = fileName;
-
-        // generate overwrite params
-        importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
-
-        importSettingsMap[collectionName] = importSettings;
-      });
+      importSettingsMap = g2gTransferReceiverService.getImportSettingMap(innerFileStats, optionsMap, operatorUserId);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
       return res.apiv3Err(new ErrorV3('Import settings invalid. See growi docs about details.', 'import_settings_invalid'));
       return res.apiv3Err(new ErrorV3('Import settings invalid. See growi docs about details.', 'import_settings_invalid'));
     }
     }
 
 
-    /*
-     * import
-     */
     try {
     try {
-      const shouldKeepUploadConfigs = configManager.getConfig('crowi', 'app:fileUploadType') !== 'none';
-
-      let savedUploadConfigs;
-      if (shouldKeepUploadConfigs) {
-        // save
-        savedUploadConfigs = Object.fromEntries(uploadConfigKeys.map((key) => {
-          return [key, configManager.getConfigFromDB('crowi', key)];
-        }));
-      }
-
-      await importService.import(collections, importSettingsMap);
-
-      // remove & save if none
-      if (shouldKeepUploadConfigs) {
-        await configManager.removeConfigsInTheSameNamespace('crowi', uploadConfigKeys);
-        await configManager.updateConfigsInTheSameNamespace('crowi', savedUploadConfigs);
-      }
-      else {
-        await configManager.updateConfigsInTheSameNamespace('crowi', sourceGROWIUploadConfigs);
-      }
-
-      await crowi?.setUpFileUpload(true);
-      await crowi?.appService?.setupAfterInstall();
+      await g2gTransferReceiverService.importCollections(collections, importSettingsMap, sourceGROWIUploadConfigs);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to import.', 'failed_to_import'), 500);
+      return res.apiv3Err(new ErrorV3('Failed to import MongoDB collections', 'mongo_collection_import_failure'), 500);
     }
     }
 
 
     return res.apiv3({ message: 'Successfully started to receive transfer data.' });
     return res.apiv3({ message: 'Successfully started to receive transfer data.' });
@@ -335,8 +287,6 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3({ transferKey: transferKeyString });
     return res.apiv3({ transferKey: transferKeyString });
   });
   });
 
 
-  // Auto export
-  // TODO: Use socket to send progress info to the client
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   pushRouter.post('/transfer', accessTokenParser, loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
   pushRouter.post('/transfer', accessTokenParser, loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const { transferKey, collections, optionsMap } = req.body;
     const { transferKey, collections, optionsMap } = req.body;
@@ -351,8 +301,7 @@ module.exports = (crowi: Crowi): Router => {
       return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
       return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
     }
     }
 
 
-    // Ask growi info
-    // TODO: Ask progress as well
+    // get growi info
     let toGROWIInfo: IDataGROWIInfo;
     let toGROWIInfo: IDataGROWIInfo;
     try {
     try {
       toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
       toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
@@ -371,7 +320,7 @@ module.exports = (crowi: Crowi): Router => {
 
 
     // Start transfer
     // Start transfer
     try {
     try {
-      await g2gTransferPusherService.startTransfer(tk, req.user, toGROWIInfo, collections, optionsMap);
+      await g2gTransferPusherService.startTransfer(tk, req.user, collections, optionsMap);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);

+ 4 - 10
packages/app/src/server/routes/apiv3/overwrite-params/pages.js

@@ -1,17 +1,11 @@
-const { pagePathUtils } = require('@growi/core');
+const { PageGrant } = require('@growi/core');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 
 
-const { isTopPage } = pagePathUtils;
-
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const ImportOptionForPages = require('~/models/admin/import-option-for-pages');
 const ImportOptionForPages = require('~/models/admin/import-option-for-pages');
 
 
 const { ObjectId } = mongoose.Types;
 const { ObjectId } = mongoose.Types;
 
 
-const {
-  GRANT_PUBLIC,
-} = mongoose.model('Page');
-
 class PageOverwriteParamsFactory {
 class PageOverwriteParamsFactory {
 
 
   /**
   /**
@@ -33,13 +27,13 @@ class PageOverwriteParamsFactory {
 
 
     params.grant = (value, { document, schema, propertyName }) => {
     params.grant = (value, { document, schema, propertyName }) => {
       if (option.makePublicForGrant2 && value === 2) {
       if (option.makePublicForGrant2 && value === 2) {
-        return GRANT_PUBLIC;
+        return PageGrant.GRANT_PUBLIC;
       }
       }
       if (option.makePublicForGrant4 && value === 4) {
       if (option.makePublicForGrant4 && value === 4) {
-        return GRANT_PUBLIC;
+        return PageGrant.GRANT_PUBLIC;
       }
       }
       if (option.makePublicForGrant5 && value === 5) {
       if (option.makePublicForGrant5 && value === 5) {
-        return GRANT_PUBLIC;
+        return PageGrant.GRANT_PUBLIC;
       }
       }
       return value;
       return value;
     };
     };

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

@@ -236,7 +236,7 @@ class ExportService {
     // TODO: remove broken zip file
     // TODO: remove broken zip file
   }
   }
 
 
-  async export(collections, shouldEmit = true) {
+  async export(collections) {
     if (this.currentProgressingStatus != null) {
     if (this.currentProgressingStatus != null) {
       throw new Error('There is an exporting process running.');
       throw new Error('There is an exporting process running.');
     }
     }
@@ -246,7 +246,7 @@ class ExportService {
 
 
     let zipFileStat;
     let zipFileStat;
     try {
     try {
-      zipFileStat = await this.exportCollectionsToZippedJson(collections, shouldEmit);
+      zipFileStat = await this.exportCollectionsToZippedJson(collections);
     }
     }
     finally {
     finally {
       this.currentProgressingStatus = null;
       this.currentProgressingStatus = null;

+ 222 - 95
packages/app/src/server/service/g2g-transfer.ts

@@ -3,12 +3,15 @@ import { basename } from 'path';
 import { Readable } from 'stream';
 import { Readable } from 'stream';
 
 
 // eslint-disable-next-line no-restricted-imports
 // eslint-disable-next-line no-restricted-imports
-import rawAxios from 'axios';
+import rawAxios, { type AxiosRequestConfig } from 'axios';
 import FormData from 'form-data';
 import FormData from 'form-data';
 import { Types as MongooseTypes } from 'mongoose';
 import { Types as MongooseTypes } from 'mongoose';
 
 
 import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
 import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
+import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import TransferKeyModel from '~/server/models/transfer-key';
 import TransferKeyModel from '~/server/models/transfer-key';
+import { generateOverwriteParams } from '~/server/routes/apiv3/import';
+import { type ImportSettings } from '~/server/service/import';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { createBatchStream } from '~/server/util/batch-stream';
 import axios from '~/utils/axios';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -18,9 +21,15 @@ import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfe
 
 
 const logger = loggerFactory('growi:service:g2g-transfer');
 const logger = loggerFactory('growi:service:g2g-transfer');
 
 
+/**
+ * Header name for transfer key
+ */
 export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
 export const X_GROWI_TRANSFER_KEY_HEADER_NAME = 'x-growi-transfer-key';
 
 
-export const uploadConfigKeys = [
+/**
+ * Keys for file upload related config
+ */
+const UPLOAD_CONFIG_KEYS = [
   'app:fileUploadType',
   'app:fileUploadType',
   'app:useOnlyEnvVarForFileUploadType',
   'app:useOnlyEnvVarForFileUploadType',
   'aws:referenceFileWithRelayMode',
   'aws:referenceFileWithRelayMode',
@@ -30,21 +39,37 @@ export const uploadConfigKeys = [
   'gcs:uploadNamespace',
   'gcs:uploadNamespace',
   'gcs:referenceFileWithRelayMode',
   'gcs:referenceFileWithRelayMode',
   'gcs:useOnlyEnvVarsForSomeOptions',
   'gcs:useOnlyEnvVarsForSomeOptions',
-];
+] as const;
+
+/**
+ * File upload related configs
+ */
+type FileUploadConfigs = { [key in typeof UPLOAD_CONFIG_KEYS[number] ]: any; }
 
 
 /**
 /**
  * Data used for comparing to/from GROWI information
  * Data used for comparing to/from GROWI information
  */
  */
 export type IDataGROWIInfo = {
 export type IDataGROWIInfo = {
+  /** GROWI version */
   version: string
   version: string
+  /** Max user count */
   userUpperLimit: number | null // Handle null as Infinity
   userUpperLimit: number | null // Handle null as Infinity
+  /** Whether file upload is disabled */
   fileUploadDisabled: boolean;
   fileUploadDisabled: boolean;
+  /** Total file size allowed */
   fileUploadTotalLimit: number | null // Handle null as Infinity
   fileUploadTotalLimit: number | null // Handle null as Infinity
+  /** Attachment infromation */
   attachmentInfo: {
   attachmentInfo: {
-    type: string,
-    bucket?: string,
-    customEndpoint?: string, // for S3
-    uploadNamespace?: string, // for GCS
+    /** File storage type */
+    type: string;
+    /** Whether the storage is writable */
+    writable: boolean;
+    /** Bucket name (S3 and GCS only) */
+    bucket?: string;
+    /** S3 custom endpoint */
+    customEndpoint?: string;
+    /** GCS namespace */
+    uploadNamespace?: string;
   };
   };
 }
 }
 
 
@@ -53,16 +78,27 @@ export type IDataGROWIInfo = {
  * TODO: mv this to "./file-uploader/uploader"
  * TODO: mv this to "./file-uploader/uploader"
  */
  */
 interface FileMeta {
 interface FileMeta {
+  /** File name */
   name: string;
   name: string;
+  /** File size in bytes */
   size: number;
   size: number;
 }
 }
 
 
 /**
 /**
  * Return type for {@link Pusher.getTransferability}
  * Return type for {@link Pusher.getTransferability}
  */
  */
-type IGetTransferabilityReturn = { canTransfer: true; } | { canTransfer: false; reason: string; };
+type Transferability = { canTransfer: true; } | { canTransfer: false; reason: string; };
 
 
+/**
+ * G2g transfer pusher
+ */
 interface Pusher {
 interface Pusher {
+  /**
+   * Merge axios config with transfer key
+   * @param {TransferKey} tk Transfer key
+   * @param {AxiosRequestConfig} config Axios config
+   */
+  generateAxiosConfig(tk: TransferKey, config: AxiosRequestConfig): AxiosRequestConfig
   /**
   /**
    * Send to-growi a request to get growi info
    * Send to-growi a request to get growi info
    * @param {TransferKey} tk Transfer key
    * @param {TransferKey} tk Transfer key
@@ -72,7 +108,7 @@ interface Pusher {
    * Check if transfering is proceedable
    * Check if transfering is proceedable
    * @param {IDataGROWIInfo} fromGROWIInfo
    * @param {IDataGROWIInfo} fromGROWIInfo
    */
    */
-  getTransferability(fromGROWIInfo: IDataGROWIInfo): Promise<IGetTransferabilityReturn>
+  getTransferability(fromGROWIInfo: IDataGROWIInfo): Promise<Transferability>
   /**
   /**
    * List files in the storage
    * List files in the storage
    * @param {TransferKey} tk Transfer key
    * @param {TransferKey} tk Transfer key
@@ -86,18 +122,22 @@ interface Pusher {
   /**
   /**
    * Start transfer data between GROWIs
    * Start transfer data between GROWIs
    * @param {TransferKey} tk TransferKey object
    * @param {TransferKey} tk TransferKey object
+   * @param {any} user User operating g2g transfer
+   * @param {IDataGROWIInfo} toGROWIInfo GROWI info of new GROWI
    * @param {string[]} collections Collection name string array
    * @param {string[]} collections Collection name string array
    * @param {any} optionsMap Options map
    * @param {any} optionsMap Options map
    */
    */
   startTransfer(
   startTransfer(
     tk: TransferKey,
     tk: TransferKey,
     user: any,
     user: any,
-    toGROWIInfo: IDataGROWIInfo,
     collections: string[],
     collections: string[],
     optionsMap: any,
     optionsMap: any,
   ): Promise<void>
   ): Promise<void>
 }
 }
 
 
+/**
+ * G2g transfer receiver
+ */
 interface Receiver {
 interface Receiver {
   /**
   /**
    * Check if key is not expired
    * Check if key is not expired
@@ -106,7 +146,7 @@ interface Receiver {
    */
    */
   validateTransferKey(key: string): Promise<void>
   validateTransferKey(key: string): Promise<void>
   /**
   /**
-   * Check if key is not expired
+   * Generate GROWIInfo
    * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
    * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
    */
    */
   answerGROWIInfo(): Promise<IDataGROWIInfo>
   answerGROWIInfo(): Promise<IDataGROWIInfo>
@@ -119,26 +159,48 @@ interface Receiver {
    */
    */
   createTransferKey(appSiteUrlOrigin: string): Promise<string>
   createTransferKey(appSiteUrlOrigin: string): Promise<string>
   /**
   /**
-   * Receive transfer request and import data.
-   * @param {Readable} zippedGROWIDataStream
-   * @returns {void}
+   * Returns a map of collection name and ImportSettings
+   * @param {any[]} innerFileStats
+   * @param {{ [key: string]: GrowiArchiveImportOption; }} optionsMap Map of collection name and GrowiArchiveImportOption
+   * @param {string} operatorUserId User ID
+   * @returns {{ [key: string]: ImportSettings; }} Map of collection name and ImportSettings
+   */
+  getImportSettingMap(
+    innerFileStats: any[],
+    optionsMap: { [key: string]: GrowiArchiveImportOption; },
+    operatorUserId: string,
+  ): { [key: string]: ImportSettings; }
+  /**
+   * Import collections
+   * @param {string} collections Array of collection name
+   * @param {{ [key: string]: ImportSettings; }} importSettingsMap Map of collection name and ImportSettings
+   * @param {FileUploadConfigs} sourceGROWIUploadConfigs File upload configs from src GROWI
+   */
+  importCollections(
+    collections: string[],
+    importSettingsMap: { [key: string]: ImportSettings; },
+    sourceGROWIUploadConfigs: FileUploadConfigs,
+  ): Promise<void>
+  /**
+   * Returns file upload configs
+   */
+  getFileUploadConfigs(): Promise<FileUploadConfigs>
+    /**
+   * Update file upload configs
+   * @param fileUploadConfigs  File upload configs
+   */
+  updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void>
+  /**
+   * Upload attachment file
+   * @param {Readable} content Pushed attachment data from source GROWI
+   * @param {any} attachmentMap Map-ped Attachment instance
    */
    */
-  receive(zippedGROWIDataStream: Readable): Promise<void>
+  receiveAttachment(content: Readable, attachmentMap: any): Promise<void>
 }
 }
 
 
-const generateAxiosRequestConfigWithTransferKey = (tk: TransferKey, additionalHeaders: {[key: string]: string} = {}) => {
-  const { appSiteUrlOrigin, key } = tk;
-
-  return {
-    baseURL: appSiteUrlOrigin,
-    headers: {
-      ...additionalHeaders,
-      [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
-    },
-    maxBodyLength: Infinity,
-  };
-};
-
+/**
+ * G2g transfer pusher
+ */
 export class G2GTransferPusherService implements Pusher {
 export class G2GTransferPusherService implements Pusher {
 
 
   crowi: any;
   crowi: any;
@@ -148,27 +210,32 @@ export class G2GTransferPusherService implements Pusher {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
+  public generateAxiosConfig(tk: TransferKey, baseConfig: AxiosRequestConfig = {}): AxiosRequestConfig {
+    const { appSiteUrlOrigin, key } = tk;
+
+    return {
+      ...baseConfig,
+      baseURL: appSiteUrlOrigin,
+      headers: {
+        ...baseConfig.headers,
+        [X_GROWI_TRANSFER_KEY_HEADER_NAME]: key,
+      },
+      maxBodyLength: Infinity,
+    };
+  }
+
   public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
   public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
-    // axios get
-    let toGROWIInfo: IDataGROWIInfo;
     try {
     try {
-      const res = await axios.get('/_api/v3/g2g-transfer/growi-info', generateAxiosRequestConfigWithTransferKey(tk));
-      toGROWIInfo = res.data.growiInfo;
+      const { data: { growiInfo } } = await axios.get('/_api/v3/g2g-transfer/growi-info', this.generateAxiosConfig(tk));
+      return growiInfo;
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
       throw new G2GTransferError('Failed to retrieve growi info.', G2GTransferErrorCode.FAILED_TO_RETRIEVE_GROWI_INFO);
       throw new G2GTransferError('Failed to retrieve growi info.', G2GTransferErrorCode.FAILED_TO_RETRIEVE_GROWI_INFO);
     }
     }
-
-    return toGROWIInfo;
   }
   }
 
 
-  /**
-   * Returns whether g2g transfer is possible and reason for failure
-   * @param toGROWIInfo to-growi info
-   * @returns Whether g2g transfer is possible and reason for failure
-   */
-  public async getTransferability(toGROWIInfo: IDataGROWIInfo): Promise<IGetTransferabilityReturn> {
+  public async getTransferability(toGROWIInfo: IDataGROWIInfo): Promise<Transferability> {
     const { fileUploadService } = this.crowi;
     const { fileUploadService } = this.crowi;
 
 
     const version = this.crowi.version;
     const version = this.crowi.version;
@@ -197,6 +264,14 @@ export class G2GTransferPusherService implements Pusher {
       };
       };
     }
     }
 
 
+    if (!toGROWIInfo.attachmentInfo.writable) {
+      return {
+        canTransfer: false,
+        // TODO: i18n for reason
+        reason: 'The storage of new Growi is not writable.',
+      };
+    }
+
     const totalFileSize = await fileUploadService.getTotalFileSize();
     const totalFileSize = await fileUploadService.getTotalFileSize();
     if ((toGROWIInfo.fileUploadTotalLimit ?? Infinity) < totalFileSize) {
     if ((toGROWIInfo.fileUploadTotalLimit ?? Infinity) < totalFileSize) {
       return {
       return {
@@ -212,7 +287,7 @@ export class G2GTransferPusherService implements Pusher {
 
 
   public async listFilesInStorage(tk: TransferKey): Promise<FileMeta[]> {
   public async listFilesInStorage(tk: TransferKey): Promise<FileMeta[]> {
     try {
     try {
-      const { data: { files } } = await axios.get<{ files: FileMeta[] }>('/_api/v3/g2g-transfer/files', generateAxiosRequestConfigWithTransferKey(tk));
+      const { data: { files } } = await axios.get<{ files: FileMeta[] }>('/_api/v3/g2g-transfer/files', this.generateAxiosConfig(tk));
       return files;
       return files;
     }
     }
     catch (err) {
     catch (err) {
@@ -291,8 +366,6 @@ export class G2GTransferPusherService implements Pusher {
           logger.warn(`Error occured when getting Attachment(ID=${attachment.id}), skipping: `, err);
           logger.warn(`Error occured when getting Attachment(ID=${attachment.id}), skipping: `, err);
           continue;
           continue;
         }
         }
-        // TODO: get attachmentLists from destination GROWI to avoid transferring files that the dest GROWI has
-        // TODO: refresh transfer key per 1 hour
         // post each attachment file data to receiver
         // post each attachment file data to receiver
         try {
         try {
           await this.doTransferAttachment(tk, attachment, fileStream);
           await this.doTransferAttachment(tk, attachment, fileStream);
@@ -305,17 +378,15 @@ export class G2GTransferPusherService implements Pusher {
   }
   }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  public async startTransfer(tk: TransferKey, user: any, toGROWIInfo: IDataGROWIInfo, collections: string[], optionsMap: any, shouldEmit = true): Promise<void> {
+  public async startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any): Promise<void> {
     const socket = this.crowi.socketIoService.getAdminSocket();
     const socket = this.crowi.socketIoService.getAdminSocket();
 
 
-    if (shouldEmit) {
-      socket.emit('admin:g2gProgress', {
-        mongo: G2G_PROGRESS_STATUS.IN_PROGRESS,
-        attachments: G2G_PROGRESS_STATUS.PENDING,
-      });
-    }
+    socket.emit('admin:g2gProgress', {
+      mongo: G2G_PROGRESS_STATUS.IN_PROGRESS,
+      attachments: G2G_PROGRESS_STATUS.PENDING,
+    });
 
 
-    const targetConfigKeys = uploadConfigKeys;
+    const targetConfigKeys = UPLOAD_CONFIG_KEYS;
 
 
     const uploadConfigs = Object.fromEntries(targetConfigKeys.map((key) => {
     const uploadConfigs = Object.fromEntries(targetConfigKeys.map((key) => {
       return [key, this.crowi.configManager.getConfig('crowi', key)];
       return [key, this.crowi.configManager.getConfig('crowi', key)];
@@ -323,8 +394,7 @@ export class G2GTransferPusherService implements Pusher {
 
 
     let zipFileStream: ReadStream;
     let zipFileStream: ReadStream;
     try {
     try {
-      const shouldEmit = false;
-      const zipFileStat = await this.crowi.exportService.export(collections, shouldEmit);
+      const zipFileStat = await this.crowi.exportService.export(collections);
       const zipFilePath = zipFileStat.zipFilePath;
       const zipFilePath = zipFileStat.zipFilePath;
 
 
       zipFileStream = createReadStream(zipFilePath);
       zipFileStream = createReadStream(zipFilePath);
@@ -350,7 +420,7 @@ export class G2GTransferPusherService implements Pusher {
       form.append('optionsMap', JSON.stringify(optionsMap));
       form.append('optionsMap', JSON.stringify(optionsMap));
       form.append('operatorUserId', user._id.toString());
       form.append('operatorUserId', user._id.toString());
       form.append('uploadConfigs', JSON.stringify(uploadConfigs));
       form.append('uploadConfigs', JSON.stringify(uploadConfigs));
-      await rawAxios.post('/_api/v3/g2g-transfer/', form, generateAxiosRequestConfigWithTransferKey(tk, form.getHeaders()));
+      await rawAxios.post('/_api/v3/g2g-transfer/', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -362,12 +432,10 @@ export class G2GTransferPusherService implements Pusher {
       throw err;
       throw err;
     }
     }
 
 
-    if (shouldEmit) {
-      socket.emit('admin:g2gProgress', {
-        mongo: G2G_PROGRESS_STATUS.COMPLETED,
-        attachments: G2G_PROGRESS_STATUS.IN_PROGRESS,
-      });
-    }
+    socket.emit('admin:g2gProgress', {
+      mongo: G2G_PROGRESS_STATUS.COMPLETED,
+      attachments: G2G_PROGRESS_STATUS.IN_PROGRESS,
+    });
 
 
     try {
     try {
       await this.transferAttachments(tk);
       await this.transferAttachments(tk);
@@ -382,31 +450,32 @@ export class G2GTransferPusherService implements Pusher {
       throw err;
       throw err;
     }
     }
 
 
-    if (shouldEmit) {
-      socket.emit('admin:g2gProgress', {
-        mongo: G2G_PROGRESS_STATUS.COMPLETED,
-        attachments: G2G_PROGRESS_STATUS.COMPLETED,
-      });
-    }
+    socket.emit('admin:g2gProgress', {
+      mongo: G2G_PROGRESS_STATUS.COMPLETED,
+      attachments: G2G_PROGRESS_STATUS.COMPLETED,
+    });
   }
   }
 
 
   /**
   /**
-   * transfer attachment to destination GROWI
-   * @param tk Transfer key
-   * @param attachment Attachment model instance
-   * @param fileStream Attachment data(loaded from storage)
+   * Transfer attachment to destination GROWI
+   * @param {TransferKey} tk Transfer key
+   * @param {any} attachment Attachment model instance
+   * @param {Readable} fileStream Attachment data(loaded from storage)
    */
    */
-  private async doTransferAttachment(tk: TransferKey, attachment, fileStream: Readable) {
+  private async doTransferAttachment(tk: TransferKey, attachment: any, fileStream: Readable): Promise<void> {
     // Use FormData to immitate browser's form data object
     // Use FormData to immitate browser's form data object
     const form = new FormData();
     const form = new FormData();
 
 
     form.append('content', fileStream, attachment.fileName);
     form.append('content', fileStream, attachment.fileName);
     form.append('attachmentMetadata', JSON.stringify(attachment));
     form.append('attachmentMetadata', JSON.stringify(attachment));
-    await rawAxios.post('/_api/v3/g2g-transfer/attachment', form, generateAxiosRequestConfigWithTransferKey(tk, form.getHeaders()));
+    await rawAxios.post('/_api/v3/g2g-transfer/attachment', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
   }
   }
 
 
 }
 }
 
 
+/**
+ * G2g transfer receiver
+ */
 export class G2GTransferReceiverService implements Receiver {
 export class G2GTransferReceiverService implements Receiver {
 
 
   crowi: any;
   crowi: any;
@@ -431,12 +500,7 @@ export class G2GTransferReceiverService implements Receiver {
     }
     }
   }
   }
 
 
-  /**
-   * generate GROWIInfo
-   * @returns
-   */
   public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
   public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
-    // TODO: add attachment file limit
     const { version, configManager, fileUploadService } = this.crowi;
     const { version, configManager, fileUploadService } = this.crowi;
     const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
     const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
     const fileUploadDisabled = configManager.getConfig('crowi', 'app:fileUploadDisabled');
     const fileUploadDisabled = configManager.getConfig('crowi', 'app:fileUploadDisabled');
@@ -445,10 +509,10 @@ export class G2GTransferReceiverService implements Receiver {
 
 
     const attachmentInfo = {
     const attachmentInfo = {
       type: configManager.getConfig('crowi', 'app:fileUploadType'),
       type: configManager.getConfig('crowi', 'app:fileUploadType'),
+      writable: isWritable,
       bucket: undefined,
       bucket: undefined,
       customEndpoint: undefined, // for S3
       customEndpoint: undefined, // for S3
       uploadNamespace: undefined, // for GCS
       uploadNamespace: undefined, // for GCS
-      writable: isWritable,
     };
     };
 
 
     // put storage location info to check storage identification
     // put storage location info to check storage identification
@@ -490,29 +554,92 @@ export class G2GTransferReceiverService implements Receiver {
     return tkd.keyString;
     return tkd.keyString;
   }
   }
 
 
-  public async receive(zipfile: Readable): Promise<void> {
-    // Import data
-    // Call onCompleteTransfer when finished
+  public getImportSettingMap(
+      innerFileStats: any[],
+      optionsMap: { [key: string]: GrowiArchiveImportOption; },
+      operatorUserId: string,
+  ): { [key: string]: ImportSettings; } {
+    const { importService } = this.crowi;
+
+    const importSettingsMap = {};
+    innerFileStats.forEach(({ fileName, collectionName }) => {
+      const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
 
 
-    return;
+      if (collectionName === 'configs' && options.mode !== 'flushAndInsert') {
+        throw new Error('`flushAndInsert` is only available as an import setting for configs collection');
+      }
+      if (collectionName === 'pages' && options.mode === 'insert') {
+        throw new Error('`insert` is not available as an import setting for pages collection');
+      }
+      if (collectionName === 'attachmentFiles.chunks') {
+        throw new Error('`attachmentFiles.chunks` must not be transferred. Please omit it from request body `collections`.');
+      }
+      if (collectionName === 'attachmentFiles.files') {
+        throw new Error('`attachmentFiles.files` must not be transferred. Please omit it from request body `collections`.');
+      }
+
+      const importSettings = importService.generateImportSettings(options.mode);
+      importSettings.jsonFileName = fileName;
+      importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
+      importSettingsMap[collectionName] = importSettings;
+    });
+
+    return importSettingsMap;
+  }
+
+  public async importCollections(
+      collections: string[],
+      importSettingsMap: { [key: string]: ImportSettings; },
+      sourceGROWIUploadConfigs: FileUploadConfigs,
+  ): Promise<void> {
+    const { configManager, importService, appService } = this.crowi;
+    /** whether to keep current file upload configs */
+    const shouldKeepUploadConfigs = configManager.getConfig('crowi', 'app:fileUploadType') !== 'none';
+
+    if (shouldKeepUploadConfigs) {
+      /** cache file upload configs */
+      const fileUploadConfigs = await this.getFileUploadConfigs();
+
+      // import mongo collections(overwrites file uplaod configs)
+      await importService.import(collections, importSettingsMap);
+
+      // restore file upload config from cache
+      await configManager.removeConfigsInTheSameNamespace('crowi', UPLOAD_CONFIG_KEYS);
+      await configManager.updateConfigsInTheSameNamespace('crowi', fileUploadConfigs);
+    }
+    else {
+      // import mongo collections(overwrites file uplaod configs)
+      await importService.import(collections, importSettingsMap);
+
+      // update file upload config
+      await configManager.updateConfigsInTheSameNamespace('crowi', sourceGROWIUploadConfigs);
+    }
+
+    await this.crowi.setUpFileUpload(true);
+    await appService.setupAfterInstall();
+  }
+
+  public async getFileUploadConfigs(): Promise<FileUploadConfigs> {
+    const { configManager } = this.crowi;
+    const fileUploadConfigs = Object.fromEntries(UPLOAD_CONFIG_KEYS.map((key) => {
+      return [key, configManager.getConfigFromDB('crowi', key)];
+    })) as FileUploadConfigs;
+
+    return fileUploadConfigs;
+  }
+
+  public async updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void> {
+    const { appService, configManager } = this.crowi;
+
+    await configManager.removeConfigsInTheSameNamespace('crowi', Object.keys(fileUploadConfigs));
+    await configManager.updateConfigsInTheSameNamespace('crowi', fileUploadConfigs);
+    await this.crowi.setUpFileUpload(true);
+    await appService.setupAfterInstall();
   }
   }
 
 
-  /**
-   *
-   * @param content Pushed attachment data from source GROWI
-   * @param attachmentMap Map-ped Attachment instance
-   * @returns
-   */
   public async receiveAttachment(content: Readable, attachmentMap): Promise<void> {
   public async receiveAttachment(content: Readable, attachmentMap): Promise<void> {
-    // TODO: test with S3, local
     const { fileUploadService } = this.crowi;
     const { fileUploadService } = this.crowi;
     return fileUploadService.uploadAttachment(content, attachmentMap);
     return fileUploadService.uploadAttachment(content, attachmentMap);
   }
   }
 
 
-  /**
-   * Sync DB, etc.
-   * @returns {Promise<void>}
-   */
-  private async onCompleteTransfer(): Promise<void> { return }
-
 }
 }

+ 1 - 1
packages/app/src/server/service/import.js

@@ -24,7 +24,7 @@ const logger = loggerFactory('growi:services:ImportService'); // eslint-disable-
 const BULK_IMPORT_SIZE = 100;
 const BULK_IMPORT_SIZE = 100;
 
 
 
 
-class ImportSettings {
+export class ImportSettings {
 
 
   constructor(mode) {
   constructor(mode) {
     this.mode = mode || 'insert';
     this.mode = mode || 'insert';