Taichi Masuyama 4 лет назад
Родитель
Сommit
cec07375ee

+ 4 - 2
packages/app/src/server/models/obsolete-page.js

@@ -684,13 +684,15 @@ export const getPageSchema = (crowi) => {
     return await findListFromBuilderAndViewer(builder, currentUser, showAnyoneKnowsLink, opt);
   };
 
-  pageSchema.statics.findListByPageIds = async function(ids, option) {
+  pageSchema.statics.findListByPageIds = async function(ids, option, excludeRedirect = true) {
     const User = crowi.model('User');
 
     const opt = Object.assign({}, option);
     const builder = new PageQueryBuilder(this.find({ _id: { $in: ids } }));
 
-    builder.addConditionToExcludeRedirect();
+    if (excludeRedirect) {
+      builder.addConditionToExcludeRedirect();
+    }
     builder.addConditionToPagenate(opt.offset, opt.limit);
 
     // count

+ 6 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -186,6 +186,7 @@ module.exports = (crowi) => {
     ],
     v5PageMigration: [
       body('action').isString().withMessage('action is required'),
+      body('pageIds').isArray().withMessage('pageIds must be an array'),
     ],
   };
 
@@ -685,18 +686,21 @@ module.exports = (crowi) => {
   });
 
   router.post('/v5-schema-migration', accessTokenParser, loginRequired, adminRequired, csrf, validator.v5PageMigration, apiV3FormValidator, async(req, res) => {
-    const { action } = req.body;
+    const { action, pageIds } = req.body;
     const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
+    const Page = crowi.model('Page');
 
     try {
       switch (action) {
         case 'initialMigration':
           if (!isV5Compatible) {
-            const Page = crowi.model('Page');
             // this method throws and emit socketIo event when error occurs
             crowi.pageService.v5InitialMigration(Page.GRANT_PUBLIC); // not await
           }
           break;
+        case 'privateLegacyPages':
+          crowi.pageService.v5MigrationByPageIds(pageIds);
+          break;
 
         default:
           logger.error(`${action} action is not supported.`);

+ 64 - 9
packages/app/src/server/service/page.js

@@ -1,5 +1,5 @@
 import { pagePathUtils } from '@growi/core';
-import Page from '~/components/Page';
+import { callbackify } from 'util';
 import loggerFactory from '~/utils/logger';
 
 const mongoose = require('mongoose');
@@ -776,6 +776,28 @@ class PageService {
     }
   }
 
+  async v5MigrationByPageIds(pageIds) {
+    const Page = this.crowi.model('Page');
+
+    if (pageIds == null || pageIds.length === 0) {
+      return;
+    }
+
+    // generate regexps
+    const regexps = await this._generateRegExpsByPageIds(pageIds);
+
+    // migrate recursively
+    try {
+      await this._v5RecursiveMigration(Page.GRANT_PUBLIC, regexps);
+    }
+    catch (err) {
+      logger.error('V5 initial miration failed.', err);
+      // socket.emit('v5InitialMirationFailed', { error: err.message }); TODO: use socket to tell user
+
+      throw err;
+    }
+  }
+
   async v5InitialMigration(grant) {
     const socket = this.crowi.socketIoService.getAdminSocket();
     try {
@@ -809,6 +831,27 @@ class PageService {
     await this._setIsV5CompatibleTrue();
   }
 
+  /*
+   * returns an array of js RegExp instance instead of RE2 instance for mongo filter
+   */
+  async _generateRegExpsByPageIds(pageIds) {
+    const Page = mongoose.model('Page');
+
+    let result;
+    try {
+      result = await Page.findListByPageIds(pageIds, null, false);
+    }
+    catch (err) {
+      logger.error('Failed to find pages by ids');
+      throw err;
+    }
+
+    const { pages } = result;
+    const regexps = pages.map(page => new RegExp(`^${page.path}`));
+
+    return regexps;
+  }
+
   async _setIsV5CompatibleTrue() {
     try {
       await this.crowi.configManager.updateConfigsInTheSameNamespace('crowi', {
@@ -823,21 +866,33 @@ class PageService {
   }
 
   // TODO: use websocket to show progress
-  async _v5RecursiveMigration(grant, rootPath) {
+  async _v5RecursiveMigration(grant, regexps) {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
     const Page = this.crowi.model('Page');
     const { PageQueryBuilder } = Page;
 
-    const total = await Page.countDocuments({ grant, parent: null });
+    // generate filter
+    let filter = {
+      grant,
+      parent: null,
+      path: { $ne: '/' },
+    };
+    if (regexps != null && regexps.length !== 0) {
+      filter = {
+        ...filter,
+        path: {
+          $in: regexps,
+        },
+      };
+    }
+
+    const total = await Page.countDocuments(filter);
 
     let baseAggregation = Page
       .aggregate([
         {
-          $match: {
-            grant,
-            parent: null,
-          },
+          $match: filter,
         },
         {
           $project: { // minimize data to fetch
@@ -921,8 +976,8 @@ class PageService {
 
     await streamToPromise(migratePagesStream);
 
-    if (await Page.exists({ grant, parent: null, path: { $ne: '/' } })) {
-      return this._v5RecursiveMigration(grant, rootPath);
+    if (await Page.exists(filter)) {
+      return this._v5RecursiveMigration(grant, regexps);
     }
 
   }

+ 48 - 0
packages/app/src/test/integration/service/page.test.js

@@ -820,5 +820,53 @@ describe('PageService', () => {
     });
   });
 
+  describe('_v5RecursiveMigration()', () => {
+    test('should migrate all pages specified by pageIds', async() => {
+      // initialize pages for test
+      const pages = await Page.insertMany([
+        {
+          path: '/private1',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1/private2',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1/private3',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+      ]);
+
+      const pageIds = pages.map(page => page._id);
+      // migrate
+      await crowi.pageService.v5MigrationByPageIds(pageIds);
+
+      const migratedPages = await Page.find({
+        path: {
+          $in: ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'],
+        },
+      });
+      const migratedPagePaths = migratedPages.filter(doc => doc.parent != null).map(doc => doc.path);
+
+      const expected = ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'];
+
+      expect(migratedPagePaths).toBe(expected);
+    });
+
+  });
+
 
 });