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

Merge remote-tracking branch 'origin/feat/g2g-nextjs' into feat/g2g-nextjs-copy-attachments-resolving-todo

Syunsuke Komma 3 лет назад
Родитель
Сommit
1e0a4ce67d

+ 59 - 21
packages/app/src/components/Admin/G2GDataTransfer.tsx

@@ -4,12 +4,15 @@ import { useTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 
 import { useGenerateTransferKeyWithThrottle } from '~/client/services/g2g-transfer';
+import { toastError } from '~/client/util/apiNotification';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { useAdminSocket } from '~/stores/socket-io';
+import customAxios from '~/utils/axios';
+
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 
-import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
+import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 
 const IGNORED_COLLECTION_NAMES = [
   'sessions', 'rlflx', 'activities',
@@ -19,13 +22,28 @@ const G2GDataTransfer = (): JSX.Element => {
   const { data: socket } = useAdminSocket();
   const { t } = useTranslation();
 
-  const [collections, setCollections] = useState<any[]>([]);
-  const [isExportModalOpen, setExportModalOpen] = useState(false);
+  const [startTransferKey, setStartTransferKey] = useState('');
+  const [collections, setCollections] = useState<string[]>([]);
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [optionsMap, setOptionsMap] = useState<any>({});
+  const [isShowExportForm, setShowExportForm] = useState(false);
   const [isExporting, setExporting] = useState(false);
   // TODO: データのエクスポートが完了したことが分かるようにする
   const [isExported, setExported] = useState(false);
 
-  const fetchData = useCallback(async() => {
+  const updateSelectedCollections = (newSelectedCollections: Set<string>) => {
+    setSelectedCollections(newSelectedCollections);
+  };
+
+  const updateOptionsMap = (newOptionsMap: any) => {
+    setOptionsMap(newOptionsMap);
+  };
+
+  const onChangeTransferKeyHandler = useCallback((e) => {
+    setStartTransferKey(e.target.value);
+  }, []);
+
+  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', {}),
@@ -37,6 +55,7 @@ const G2GDataTransfer = (): JSX.Element => {
     });
 
     setCollections(filteredCollections);
+    setSelectedCollections(new Set(filteredCollections));
     setExporting(statusData.status.isExporting);
   }, []);
 
@@ -73,30 +92,57 @@ const G2GDataTransfer = (): JSX.Element => {
     generateTransferKeyWithThrottle();
   }, [generateTransferKeyWithThrottle]);
 
-  const transferData = () => {
-    // データ移行の処理
-  };
+  const startTransfer = useCallback(async(e) => {
+    e.preventDefault();
 
-  const exportingRequestedHandler = useCallback(() => {}, []);
+    try {
+      await customAxios.post('/_api/v3/g2g-transfer/transfer', {
+        transferKey: startTransferKey,
+        collections,
+        optionsMap,
+      });
+    }
+    catch (errs) {
+      toastError('Failed to transfer');
+    }
+  }, [startTransferKey, collections, optionsMap]);
 
   useEffect(() => {
-    fetchData();
+    setCollectionsAndSelectedCollections();
 
     setupWebsocketEventHandler();
-  }, [fetchData, setupWebsocketEventHandler]);
+  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler]);
 
   return (
     <div data-testid="admin-export-archive-data">
       <h2 className="border-bottom">{t('admin:g2g_data_transfer.transfer_data_to_another_growi')}</h2>
 
-      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isExporting} onClick={() => setExportModalOpen(true)}>
+      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isExporting} onClick={() => setShowExportForm(!isShowExportForm)}>
         {t('admin:g2g_data_transfer.advanced_options')}
       </button>
 
-      <form onSubmit={transferData}>
+      {collections.length !== 0 && (
+        <div className={isShowExportForm ? '' : 'd-none'}>
+          <G2GDataTransferExportForm
+            allCollectionNames={collections}
+            selectedCollections={selectedCollections}
+            updateSelectedCollections={updateSelectedCollections}
+            optionsMap={optionsMap}
+            updateOptionsMap={updateOptionsMap}
+          />
+        </div>
+      )}
+
+      <form onSubmit={startTransfer}>
         <div className="form-group row mt-3">
           <div className="col-9">
-            <input className="form-control" type="text" placeholder={t('admin:g2g_data_transfer.paste_transfer_key')} />
+            <input
+              className="form-control"
+              type="text"
+              placeholder={t('admin:g2g_data_transfer.paste_transfer_key')}
+              onChange={onChangeTransferKeyHandler}
+              required
+            />
           </div>
           <div className="col-3">
             <button type="submit" className="btn btn-primary w-100">{t('admin:g2g_data_transfer.start_transfer')}</button>
@@ -125,14 +171,6 @@ const G2GDataTransfer = (): JSX.Element => {
         <p className="mb-1">{t('admin:g2g_data_transfer.once_transfer_key_used')}</p>
         <p className="mb-0">{t('admin:g2g_data_transfer.transfer_to_growi_cloud')}</p>
       </div>
-
-      <SelectCollectionsModal
-        isOpen={isExportModalOpen}
-        onExportingRequested={exportingRequestedHandler}
-        onClose={() => setExportModalOpen(false)}
-        collections={collections}
-        isAllChecked
-      />
     </div>
   );
 };

+ 304 - 0
packages/app/src/components/Admin/G2GDataTransferExportForm.tsx

@@ -0,0 +1,304 @@
+import React, {
+  useState, useEffect, useCallback, useMemo,
+} from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import ImportOptionForPages from '~/models/admin/import-option-for-pages';
+import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
+// import { useAdminSocket } from '~/stores/socket-io';
+
+import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
+import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
+
+const GROUPS_PAGE = [
+  'pages', 'revisions', 'tags', 'pagetagrelations',
+];
+const GROUPS_USER = [
+  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+];
+const GROUPS_CONFIG = [
+  'configs', 'updateposts', 'globalnotificationsettings',
+];
+const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+
+const IMPORT_OPTION_CLASS_MAPPING = {
+  pages: ImportOptionForPages,
+  revisions: ImportOptionForRevisions,
+};
+
+type Props = {
+  allCollectionNames: string[],
+  selectedCollections: Set<string>,
+  updateSelectedCollections: (newSelectedCollections: Set<string>) => void,
+  optionsMap: any,
+  updateOptionsMap: (newOptionsMap: any) => void,
+};
+
+const G2GDataTransferExportForm = (props: Props): JSX.Element => {
+  // const { data: socket } = useAdminSocket();
+  const { t } = useTranslation('admin');
+
+  const {
+    allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
+  } = props;
+
+  // const [isImporting, setImporting] = useState(false);
+  // const [isImported, setImported] = useState(false);
+  // const [progressMap, setProgressMap] = useState<any>({});
+  // const [errorsMap, setErrorsMap] = useState<any>([]);
+  const [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
+  const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
+
+  const checkAll = useCallback(() => {
+    updateSelectedCollections(new Set(allCollectionNames));
+  }, [allCollectionNames, updateSelectedCollections]);
+
+  const uncheckAll = useCallback(() => {
+    updateSelectedCollections(new Set());
+  }, [updateSelectedCollections]);
+
+  const updateOption = (collectionName, data) => {
+    const options = optionsMap[collectionName];
+
+    // merge
+    Object.assign(options, data);
+
+    const updatedOptionsMap = {};
+    updatedOptionsMap[collectionName] = options;
+    updateOptionsMap((prev) => {
+      return { ...prev, updatedOptionsMap };
+    });
+  };
+
+  const ImportItems = ({ collectionNames }): JSX.Element => {
+    const toggleCheckbox = (collectionName, bool) => {
+      const collections = new Set(selectedCollections);
+      if (bool) {
+        collections.add(collectionName);
+      }
+      else {
+        collections.delete(collectionName);
+      }
+
+      updateSelectedCollections(collections);
+
+      // TODO: validation
+      // this.validate();
+    };
+
+    const openConfigurationModal = (collectionName) => {
+      setConfigurationModalOpen(true);
+      setCollectionNameForConfiguration(collectionName);
+    };
+
+    return (
+      <div className="row">
+        {collectionNames.map((collectionName) => {
+          // const collectionProgress = progressMap[collectionName];
+          // const errors = errorsMap[collectionName];
+          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
+
+          if (optionsMap[collectionName] == null) {
+            return <></>;
+          }
+
+          return (
+            <div className="col-md-6 my-1" key={collectionName}>
+              <ImportCollectionItem
+                // isImporting={isImporting}
+                // isImported={collectionProgress ? isImported : false}
+                // insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
+                // modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
+                // errorsCount={errors ? errors.length : 0}
+                isImporting={0}
+                isImported={false}
+                insertedCount={0}
+                modifiedCount={0}
+                errorsCount={0}
+                collectionName={collectionName}
+                isSelected={selectedCollections.has(collectionName)}
+                option={optionsMap[collectionName]}
+                isConfigButtonAvailable={isConfigButtonAvailable}
+                // events
+                onChange={toggleCheckbox}
+                onOptionChange={updateOption}
+                onConfigButtonClicked={openConfigurationModal}
+                // TODO: show progress
+                isHideProgress
+              />
+            </div>
+          );
+        })}
+      </div>
+    );
+  };
+
+  const WarnForGroups = ({ errors, key }): JSX.Element => {
+    if (errors.length === 0) {
+      return <></>;
+    }
+
+    return (
+      <div key={key} className="alert alert-warning">
+        <ul>
+          {errors.map((error, index) => {
+            // eslint-disable-next-line react/no-array-index-key
+            return <li key={`${key}-${index}`}>{error}</li>;
+          })}
+        </ul>
+      </div>
+    );
+  };
+
+  const GroupImportItems = ({ groupList, groupName, errors }): JSX.Element => {
+    const collectionNames = groupList.filter((groupCollectionName) => {
+      return allCollectionNames.includes(groupCollectionName);
+    });
+
+    if (collectionNames.length === 0) {
+      return <></>;
+    }
+
+    return (
+      <div className="mt-4">
+        <legend>{groupName} Collections</legend>
+        <ImportItems collectionNames={collectionNames} />
+        <WarnForGroups errors={errors} key={`warnFor${groupName}`} />
+      </div>
+    );
+  };
+
+  const OtherImportItems = (): JSX.Element => {
+    const collectionNames = allCollectionNames.filter((collectionName) => {
+      return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
+    });
+
+    // TODO: エラー対応
+    return <GroupImportItems groupList={collectionNames} groupName='Other' errors={[]} />;
+  };
+
+  // TODO: モーダルを表示できるようにする
+  const configurationModal = useMemo(() => {
+    console.log(isConfigurationModalOpen);
+    if (collectionNameForConfiguration == null) {
+      return <></>;
+    }
+
+    return (
+      <ImportCollectionConfigurationModal
+        isOpen={isConfigurationModalOpen}
+        onClose={setConfigurationModalOpen(false)}
+        onOptionChange={updateOption}
+        collectionName={collectionNameForConfiguration}
+        option={optionsMap[collectionNameForConfiguration]}
+      />
+    );
+  }, [collectionNameForConfiguration, isConfigurationModalOpen]);
+
+  const setInitialOptionsMap = () => {
+    const initialOptionsMap = {};
+    allCollectionNames.forEach((collectionName) => {
+      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
+        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+        : DEFAULT_MODE;
+      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      initialOptionsMap[collectionName] = new ImportOption(initialMode);
+    });
+    updateOptionsMap(initialOptionsMap);
+  };
+
+  // TODO: use Socket
+
+  // setupWebsocketEventHandler() {
+  //   const socket = this.props.adminSocketIoContainer.getSocket();
+
+  //   // websocket event
+  //   // eslint-disable-next-line object-curly-newline
+  //   socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
+  //     const { progressMap, errorsMap } = this.state;
+  //     progressMap[collectionName] = collectionProgress;
+
+  //     const errors = errorsMap[collectionName] || [];
+  //     errorsMap[collectionName] = errors.concat(appendedErrors);
+
+  //     this.setState({
+  //       isImporting: true,
+  //       progressMap,
+  //       errorsMap,
+  //     });
+  //   });
+
+  //   // websocket event
+  //   socket.on('admin:onTerminateForImport', () => {
+  //     this.setState({
+  //       isImporting: false,
+  //       isImported: true,
+  //     });
+
+  //     toastSuccess(undefined, 'Import process has completed.');
+  //   });
+
+  //   // websocket event
+  //   socket.on('admin:onErrorForImport', (err) => {
+  //     this.setState({
+  //       isImporting: false,
+  //       isImported: false,
+  //     });
+
+  //     toastError(err, 'Import process has failed.');
+  //   });
+  // }
+
+  // teardownWebsocketEventHandler() {
+  //   const socket = this.props.adminSocketIoContainer.getSocket();
+
+  //   socket.removeAllListeners('admin:onProgressForImport');
+  //   socket.removeAllListeners('admin:onTerminateForImport');
+  // }
+
+  useEffect(() => {
+    setInitialOptionsMap();
+    // setupWebsocketEventHandler();
+    // teardownWebsocketEventHandler();
+  }, []);
+
+  return (
+    <>
+      <form className="form-inline mt-4">
+        <div className="form-group">
+          <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={checkAll}>
+            <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+          </button>
+        </div>
+        <div className="form-group">
+          <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={uncheckAll}>
+            <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+          </button>
+        </div>
+      </form>
+
+      <div className="card well small my-4">
+        <ul>
+          <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+          <ul>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+          </ul>
+        </ul>
+      </div>
+
+      {/* TODO: エラー追加 */}
+      <GroupImportItems groupList={GROUPS_PAGE} groupName='Page' errors={[]} />
+      <GroupImportItems groupList={GROUPS_USER} groupName='User' errors={[]} />
+      <GroupImportItems groupList={GROUPS_CONFIG} groupName='Config' errors={[]} />
+      <OtherImportItems />
+
+      {configurationModal}
+    </>
+  );
+};
+
+export default G2GDataTransferExportForm;

+ 4 - 2
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -17,6 +17,7 @@ export const DEFAULT_MODE = 'insert';
 export const MODE_RESTRICTED_COLLECTION = {
   configs: ['flushAndInsert'],
   users: ['insert', 'upsert'],
+  pages: ['upsert', 'flushAndInsert'],
 };
 
 export default class ImportCollectionItem extends React.Component {
@@ -194,7 +195,7 @@ export default class ImportCollectionItem extends React.Component {
 
   render() {
     const {
-      isSelected,
+      isSelected, isHideProgress,
     } = this.props;
 
     return (
@@ -210,7 +211,7 @@ export default class ImportCollectionItem extends React.Component {
             </span>
           </div>
         </div>
-        {isSelected && (
+        {isSelected && !isHideProgress && (
           <>
             {this.renderProgressBar()}
             <div className="card-body">{this.renderBody()}</div>
@@ -225,6 +226,7 @@ export default class ImportCollectionItem extends React.Component {
 ImportCollectionItem.propTypes = {
   collectionName: PropTypes.string.isRequired,
   isSelected: PropTypes.bool.isRequired,
+  isHideProgress: PropTypes.bool,
   option: PropTypes.instanceOf(GrowiArchiveImportOption).isRequired,
 
   isImporting: PropTypes.bool.isRequired,

+ 2 - 1
packages/app/src/interfaces/transfer-key.ts

@@ -1,5 +1,6 @@
 export interface ITransferKey<ID = string> {
   _id: ID
   expireAt: Date
-  value: string,
+  keyString: string,
+  key: string,
 }

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

@@ -8,14 +8,15 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 const logger = loggerFactory('growi:models:transfer-key');
 
 interface ITransferKeyMethods {
-  findOneActiveTransferKey(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
+  findOneActiveTransferKey(key: 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 },
+  keyString: { type: String, unique: true }, // original key string
+  key: { type: String, unique: true },
 }, {
   timestamps: {
     createdAt: true,
@@ -23,17 +24,8 @@ const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
   },
 });
 
-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;
+schema.statics.findOneActiveTransferKey = async function(key: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
+  return this.findOne({ key });
 };
 
 export default getOrCreateModel('TransferKey', schema);

+ 56 - 74
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -103,15 +103,15 @@ module.exports = (crowi: Crowi): Router => {
 
   // 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];
+    const key = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME];
 
-    if (typeof transferKeyString !== 'string') {
+    if (typeof key !== '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
+      transferKey = await (TransferKeyModel as any).findOneActiveTransferKey(key); // TODO: Improve TS of models
     }
     catch (err) {
       logger.error(err);
@@ -124,7 +124,7 @@ module.exports = (crowi: Crowi): Router => {
 
     // Inject transferKey to req
     try {
-      req.transferKey = TransferKey.parse(transferKey.value);
+      req.transferKey = TransferKey.parse(transferKey.keyString);
     }
     catch (err) {
       logger.error(err);
@@ -140,14 +140,14 @@ module.exports = (crowi: Crowi): Router => {
 
   // Auto import
   // eslint-disable-next-line max-len
-  receiveRouter.post('/', uploads.single('transferDataZipFile'), /* verifyAndExtractTransferKey, */ async(req: Request & { transferKey: TransferKey, operatorUserId: string }, res: ApiV3Response) => {
+  receiveRouter.post('/', uploads.single('transferDataZipFile'), verifyAndExtractTransferKey, async(req: Request & { transferKey: TransferKey, operatorUserId: string }, res: ApiV3Response) => {
     const { file } = req;
 
     const zipFile = importService.getFile(file.filename);
-    let data;
 
     const { collections: strCollections, optionsMap: strOptionsMap, operatorUserId } = req.body;
 
+    // Parse multipart form data
     let collections;
     let optionsMap;
     try {
@@ -175,88 +175,70 @@ module.exports = (crowi: Crowi): Router => {
     }
     catch (err) {
       logger.error(err);
-      // adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
+      return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
     }
 
-    /*
-     * validate with meta.json
-     */
     try {
+      // validate with meta.json
       importService.validate(meta);
     }
     catch (err) {
       logger.error(err);
-      // adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
+
+      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);
     }
 
     // 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;
+    try {
+      innerFileStats.forEach(({ fileName, collectionName }) => {
+        // instanciate GrowiArchiveImportOption
+        const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
 
-      // generate overwrite params
-      importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
+        // 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');
+        }
 
-      importSettingsMap[collectionName] = importSettings;
-    });
+        const importSettings = importService.generateImportSettings(options.mode);
 
-    /*
-     * 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 });
-    }
+        importSettings.jsonFileName = fileName;
 
-    // -----
+        // generate overwrite params
+        importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
 
-    try {
-      data = await growiBridgeService.parseZipFile(zipFile);
+        importSettingsMap[collectionName] = importSettings;
+      });
     }
     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);
+      return res.apiv3Err(new ErrorV3('Import settings invalid. See growi docs about details.', 'import_settings_invalid'));
     }
 
+    /*
+     * import
+     */
     try {
-      await g2gTransferReceiverService.receive(file.stream);
+      importService.import(collections, importSettingsMap);
     }
     catch (err) {
       logger.error(err);
-      return res.apiv3Err(new ErrorV3('Error occurred while importing transfer data.', 'failed_to_receive'));
+      return;
     }
 
+    // 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.' });
   });
 
@@ -346,21 +328,21 @@ module.exports = (crowi: Crowi): Router => {
 
     // Ask growi info
     // TODO: Ask progress as well
-    // let toGROWIInfo: IDataGROWIInfo;
-    // try {
-    //   toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
-    // }
-    // catch (err) {
-    //   logger.error(err);
-    //   return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI growi info.', 'failed_to_ask_growi_info'));
-    // }
+    let toGROWIInfo: IDataGROWIInfo;
+    try {
+      toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI growi info.', 'failed_to_ask_growi_info'));
+    }
 
     // Check if can transfer
-    // const canTransfer = await g2gTransferPusherService.canTransfer(toGROWIInfo);
-    // if (!canTransfer) {
-    //   logger.debug('Could not transfer.');
-    //   return res.apiv3Err(new ErrorV3('GROWI is incompatible to transfer data.', 'growi_incompatible_to_transfer'));
-    // }
+    const canTransfer = await g2gTransferPusherService.canTransfer(toGROWIInfo);
+    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 {

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

@@ -65,6 +65,7 @@ interface Receiver {
    */
   answerGROWIInfo(): Promise<IDataGROWIInfo>
   /**
+   * DO NOT USE TransferKeyModel.create() directly, instead, use this method to create a TransferKey document.
    * 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
@@ -137,12 +138,8 @@ export class G2GTransferPusherService implements Pusher {
     // axios get
     let toGROWIInfo: IDataGROWIInfo;
     try {
-      const res = await axios.get('/_api/v3/g2g-transfer/growi-info', generateAxiosRequestConfigWithTransferKey(tk));
-      toGROWIInfo = {
-        userUpperLimit: res.data.userUpperLimit,
-        version: res.data.version,
-        attachmentInfo: res.data.attachmentInfo,
-      };
+      const res = await axios.get('/_api/v3/g2g-transfer/growi-info', generateAxiosRequestConfigWithTransfer(tk));
+      toGROWIInfo = res.data.growiInfo;
     }
     catch (err) {
       logger.error(err);
@@ -153,9 +150,19 @@ export class G2GTransferPusherService implements Pusher {
   }
 
   public async canTransfer(toGROWIInfo: IDataGROWIInfo): Promise<boolean> {
-    // Compare GROWIInfos
+    const configManager = this.crowi.configManager;
+    const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
+    const version = this.crowi.version;
 
-    return false;
+    if (version !== toGROWIInfo.version) {
+      return false;
+    }
+
+    if ((userUpperLimit ?? Infinity) < (toGROWIInfo.userUpperLimit ?? 0)) {
+      return false;
+    }
+
+    return true;
   }
 
   public async transferAttachments(tk: TransferKey): Promise<void> {
@@ -301,14 +308,14 @@ export class G2GTransferReceiverService implements Receiver {
     // Save TransferKey document
     let tkd;
     try {
-      tkd = await TransferKeyModel.create({ _id: uuid, value: transferKeyString });
+      tkd = await TransferKeyModel.create({ _id: uuid, keyString: transferKeyString, key: uuid });
     }
     catch (err) {
       logger.error(err);
       throw err;
     }
 
-    return tkd.value;
+    return tkd.keyString;
   }
 
   public async receive(zipfile: Readable): Promise<void> {

+ 4 - 0
packages/app/src/utils/vo/transfer-key.ts

@@ -14,6 +14,10 @@ export class TransferKey {
     this.key = key;
   }
 
+  get getKeyString(): string {
+    return TransferKey.generateKeyString(this.key, this.appUrl);
+  }
+
   /**
    * Parse a transfer key string generated by the generateKeyString static method
    * @param {string} keyString Transfer key string