Taichi Masuyama před 4 roky
rodič
revize
b6659690da

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

@@ -37,6 +37,10 @@ const STATUS_PUBLISHED = 'published';
 const STATUS_DELETED = 'deleted';
 const STATUS_DELETED = 'deleted';
 
 
 const pageSchema = new mongoose.Schema({
 const pageSchema = new mongoose.Schema({
+  parent: {
+    type: ObjectId, ref: 'Page', default: null,
+  },
+  isEmpty: { type: Boolean, default: false },
   path: {
   path: {
     type: String, required: true, index: true, unique: true,
     type: String, required: true, index: true, unique: true,
   },
   },

+ 15 - 0
packages/app/src/server/routes/apiv3/pages.js

@@ -680,5 +680,20 @@ module.exports = (crowi) => {
     }
     }
 
 
   });
   });
+
+  router.get('/v5-schema-migration', /* accessTokenParser, loginRequired, adminRequired, csrf, */ async(req, res) => {
+
+    try {
+      const Page = crowi.model('Page');
+      await crowi.pageService.v5RecursiveMigration(Page.GRANT_PUBLIC);
+    }
+    catch (err) {
+      logger.error('Error\n', err);
+      return res.apiv3Err(new ErrorV3('Failed to migrate pages. Please try again.', 'v5_migration_failed'), 500);
+    }
+
+    return res.apiv3({});
+  });
+
   return router;
   return router;
 };
 };

+ 82 - 0
packages/app/src/server/service/page.js

@@ -4,6 +4,7 @@ import loggerFactory from '~/utils/logger';
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 const escapeStringRegexp = require('escape-string-regexp');
 const escapeStringRegexp = require('escape-string-regexp');
 const streamToPromise = require('stream-to-promise');
 const streamToPromise = require('stream-to-promise');
+const pathlib = require('path');
 
 
 const logger = loggerFactory('growi:models:page');
 const logger = loggerFactory('growi:models:page');
 const debug = require('debug')('growi:models:page');
 const debug = require('debug')('growi:models:page');
@@ -737,6 +738,87 @@ class PageService {
     }
     }
   }
   }
 
 
+  async v5RecursiveMigration(grant, rootPath = null) {
+    const BATCH_SIZE = 100;
+    const Page = this.crowi.model('Page');
+
+    let randomPagesStream = await Page
+      .aggregate([
+        {
+          $sample: {
+            size: BATCH_SIZE,
+          },
+        },
+        {
+          $match: {
+            grant,
+            parent: null,
+          },
+        },
+        {
+          $project: { // minimize data to fetch
+            _id: 1,
+            path: 1,
+          },
+        },
+      ])
+      .cursor({ batchSize: BATCH_SIZE }) // get stream
+      .exec();
+
+    const batchStream = createBatchStream(BATCH_SIZE);
+
+    const migratePagesStream = new Writable({
+      objectMode: true,
+      async write(pages, encoding, callback) {
+        // bulkWrite to update parent
+        const updateManyOperations = await Promise.all(pages.map(async(page) => {
+          const parentPath = pathlib.dirname(page.path);
+
+          let parent = await Page.findOne({ path: parentPath }).select({ _id: 1 }).lean().exec();
+          if (parent == null) {
+            try {
+              parent = await (new Page({ path: parentPath, isEmpty: true })).save();
+            }
+            catch (err) {
+              logger.error('Couldnt create parent', err);
+              throw err;
+            }
+          }
+
+          return {
+            updateMany: {
+              filter: {
+                path: new RegExp(`^\\/${parentPath}\\/[^/]+\\/?$`), // ex. /parent/any_child
+              },
+              update: {
+                parent,
+              },
+            },
+          };
+        }));
+
+        await Page.bulkWrite(updateManyOperations);
+
+        callback();
+      },
+      final(callback) {
+        callback();
+      },
+    });
+
+    randomPagesStream
+      .pipe(batchStream)
+      .pipe(migratePagesStream);
+
+    await streamToPromise(migratePagesStream);
+
+    randomPagesStream = null;
+
+    if ((await Page.exists({ grant, parent: null, path: { $ne: '/' } }))) {
+      await this.v5RecursiveMigration(grant, rootPath);
+    }
+  }
+
 }
 }
 
 
 module.exports = PageService;
 module.exports = PageService;