Yuki Takei 1 год назад
Родитель
Сommit
aead20f7a6

+ 3 - 1
apps/app/package.json

@@ -217,7 +217,8 @@
   "// comments for defDependencies": {
     "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798",
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
-    "handsontable": "v7.0.0 or above is no loger MIT lisence."
+    "handsontable": "v7.0.0 or above is no loger MIT lisence.",
+    "mongodb": "mongoose which is used requires mongo@4.16.0."
   },
   "devDependencies": {
     "@growi/core-styles": "link:../../packages/core-styles",
@@ -261,6 +262,7 @@
     "jest-localstorage-mock": "^2.4.14",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
+    "mongodb": "4.16.0",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",

+ 69 - 35
apps/app/src/server/service/import/import.ts

@@ -1,28 +1,32 @@
-/**
- * @typedef {import("@types/unzip-stream").Parse} Parse
- * @typedef {import("@types/unzip-stream").Entry} Entry
- */
-
 import fs from 'fs';
 import path from 'path';
+import type { EventEmitter } from 'stream';
 import { Writable, Transform } from 'stream';
 
 import JSONStream from 'JSONStream';
 import gc from 'expose-gc/function';
+import type {
+  BulkWriteOperationError, BulkWriteResult, ObjectId, UnorderedBulkOperation,
+} from 'mongodb';
 import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 import unzipStream from 'unzip-stream';
 
+import type Crowi from '~/server/crowi';
 import { setupIndependentModels } from '~/server/crowi/setup-models';
+import type CollectionProgress from '~/server/models/vo/collection-progress';
 import loggerFactory from '~/utils/logger';
 
 import CollectionProgressingStatus from '../../models/vo/collection-progressing-status';
 import { createBatchStream } from '../../util/batch-stream';
+import { configManager } from '../config-manager';
 
+import type { ConvertMap } from './construct-convert-map';
 import { constructConvertMap } from './construct-convert-map';
 import { getModelFromCollectionName } from './get-model-from-collection-name';
 import { keepOriginal } from './overwrite-function';
 
+
 const logger = loggerFactory('growi:services:ImportService'); // eslint-disable-line no-unused-vars
 
 
@@ -31,6 +35,8 @@ const BULK_IMPORT_SIZE = 100;
 
 class ImportingCollectionError extends Error {
 
+  collectionProgress: CollectionProgress;
+
   constructor(collectionProgress, error) {
     super(error);
     this.collectionProgress = collectionProgress;
@@ -41,17 +47,31 @@ class ImportingCollectionError extends Error {
 
 export class ImportService {
 
-  constructor(crowi) {
+  private crowi: Crowi;
+
+  private growiBridgeService: any;
+
+  private adminEvent: EventEmitter;
+
+  private currentProgressingStatus: CollectionProgressingStatus | null;
+
+  private convertMap: ConvertMap;
+
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.growiBridgeService = crowi.growiBridgeService;
-    this.getFile = this.growiBridgeService.getFile.bind(this);
-    this.baseDir = path.join(crowi.tmpDir, 'imports');
+    // this.getFile = this.growiBridgeService.getFile.bind(this);
+    // this.baseDir = path.join(crowi.tmpDir, 'imports');
 
     this.adminEvent = crowi.event('admin');
 
     this.currentProgressingStatus = null;
   }
 
+  private get baseDir(): string {
+    return path.join(this.crowi.tmpDir, 'imports');
+  }
+
   /**
    * parse all zip files in downloads dir
    *
@@ -62,9 +82,9 @@ export class ImportService {
     const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
 
     // process serially so as not to waste memory
-    const zipFileStats = [];
-    const parseZipFilePromises = zipFiles.map((file) => {
-      const zipFile = this.getFile(file);
+    const zipFileStats: any[] = [];
+    const parseZipFilePromises: Promise<any>[] = zipFiles.map((file) => {
+      const zipFile = this.growiBridgeService.getFile(file);
       return this.growiBridgeService.parseZipFile(zipFile);
     });
     for await (const stat of parseZipFilePromises) {
@@ -77,8 +97,6 @@ export class ImportService {
     // sort with ctime("Change Time" - Time when file status was last changed (inode data modification).)
     filtered.sort((a, b) => { return a.fileStat.ctime - b.fileStat.ctime });
 
-    const isImporting = this.currentProgressingStatus != null;
-
     const zipFileStat = filtered.pop();
     let isTheSameVersion = false;
 
@@ -97,8 +115,8 @@ export class ImportService {
     return {
       isTheSameVersion,
       zipFileStat,
-      isImporting,
-      progressList: isImporting ? this.currentProgressingStatus.progressList : null,
+      isImporting: this.currentProgressingStatus != null,
+      progressList: this.currentProgressingStatus?.progressList ?? null,
     };
   }
 
@@ -107,7 +125,6 @@ export class ImportService {
     await setupIndependentModels();
 
     // initialize convertMap
-    /** @type {import('./construct-convert-map').ConvertMap} */
     this.convertMap = constructConvertMap();
   }
 
@@ -144,9 +161,9 @@ export class ImportService {
     this.currentProgressingStatus = null;
     this.emitTerminateEvent();
 
-    await this.crowi.configManager.loadConfigs();
+    await configManager.loadConfigs();
 
-    const currentIsV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
+    const currentIsV5Compatible = configManager.getConfig('crowi', 'app:isV5Compatible');
     const isImportPagesCollection = collections.includes('pages');
     const shouldNormalizePages = currentIsV5Compatible && isImportPagesCollection;
 
@@ -162,6 +179,10 @@ export class ImportService {
    * @return {insertedIds: Array.<string>, failedIds: Array.<string>}
    */
   async importCollection(collectionName, importSettings) {
+    if (this.currentProgressingStatus == null) {
+      throw new Error('Something went wrong: currentProgressingStatus is not initialized');
+    }
+
     // prepare functions invoked from custom streams
     const convertDocuments = this.convertDocuments.bind(this);
     const bulkOperate = this.bulkOperate.bind(this);
@@ -174,7 +195,7 @@ export class ImportService {
     const collectionProgress = this.currentProgressingStatus.progressMap[collectionName];
 
     try {
-      const jsonFile = this.getFile(jsonFileName);
+      const jsonFile = this.growiBridgeService.getFile(jsonFileName);
 
       // validate options
       this.validateImportSettings(collectionName, importSettings);
@@ -328,7 +349,7 @@ export class ImportService {
   async unzip(zipFile) {
     const readStream = fs.createReadStream(zipFile);
     const unzipStreamPipe = readStream.pipe(unzipStream.Parse());
-    const files = [];
+    const files: string[] = [];
 
     unzipStreamPipe.on('entry', (/** @type {Entry} */ entry) => {
       const fileName = entry.path;
@@ -362,34 +383,47 @@ export class ImportService {
    * execute unorderedBulkOp and ignore errors
    *
    * @memberOf ImportService
-   * @param {object} unorderedBulkOp result of Model.collection.initializeUnorderedBulkOp()
-   * @return {object} e.g. { insertedCount: 10, errors: [...] }
    */
-  async execUnorderedBulkOpSafely(unorderedBulkOp) {
-    let errors = [];
-    let result = null;
+  async execUnorderedBulkOpSafely(unorderedBulkOp: UnorderedBulkOperation): Promise<{ insertedCount: number, modifiedCount: number, errors: unknown[] }> {
+    let errors: unknown[] = [];
+    let log: BulkWriteResult | null = null;
 
     try {
-      const log = await unorderedBulkOp.execute();
-      result = log.result;
+      log = await unorderedBulkOp.execute();
     }
     catch (err) {
-      result = err.result;
-      errors = err.writeErrors || [err];
-      errors.map((err) => {
-        const moreDetailErr = err.err;
-        return { _id: moreDetailErr.op._id, message: err.errmsg };
+
+      const _errs = Array.isArray(err.writeErrors) ? err : [err];
+
+      const errTypeGuard = (err: any): err is BulkWriteOperationError => {
+        return 'index' in err;
+      };
+      const docTypeGuard = (op: any): op is { _id: ObjectId } => {
+        return '_id' in op;
+      };
+
+      errors = _errs.map((e) => {
+        if (errTypeGuard(e)) {
+          const { op } = e;
+          return {
+            _id: docTypeGuard(op) ? op._id : undefined,
+            message: err.errmsg,
+          };
+        }
+        return err;
       });
     }
 
-    const insertedCount = result.nInserted + result.nUpserted;
-    const modifiedCount = result.nModified;
+    assert(log != null);
+    const insertedCount = log.nInserted + log.nUpserted;
+    const modifiedCount = log.nModified;
 
     return {
       insertedCount,
       modifiedCount,
       errors,
     };
+
   }
 
   /**
@@ -403,7 +437,7 @@ export class ImportService {
    */
   convertDocuments(collectionName, document, overwriteParams) {
     const Model = getModelFromCollectionName(collectionName);
-    const schema = (Model != null) ? Model.schema : null;
+    const schema = (Model != null) ? Model.schema : undefined;
     const convertMap = this.convertMap[collectionName];
 
     const _document = {};

+ 1 - 1
apps/app/src/server/service/page/index.ts

@@ -3057,7 +3057,7 @@ class PageService implements IPageService {
     return isUnique;
   }
 
-  async normalizeAllPublicPages() {
+  async normalizeAllPublicPages(): Promise<void> {
     let isUnique;
     try {
       isUnique = await this._isPagePathIndexUnique();

+ 1 - 0
apps/app/src/server/service/page/page-service.ts

@@ -24,6 +24,7 @@ export interface IPageService {
   findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>,
   shortBodiesMapByPageIds(pageIds?: Types.ObjectId[], user?): Promise<Record<string, string | null>>,
   constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): IPageInfo | Omit<IPageInfoForEntity, 'bookmarkCount'>,
+  normalizeAllPublicPages(): Promise<void>,
   canDelete(page: PageDocument, creatorId: ObjectIdLike | null, operator: any | null, isRecursively: boolean): boolean,
   canDeleteCompletely(
     page: PageDocument, creatorId: ObjectIdLike | null, operator: any | null, isRecursively: boolean, userRelatedGroups: PopulatedGrantedGroup[]