ryoji-s 2 лет назад
Родитель
Сommit
0743d7556d

+ 3 - 1
apps/app/src/server/events/user.ts

@@ -7,6 +7,8 @@ import mongoose from 'mongoose';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
+import { deleteCompletelyUserHomeBySystem } from '../service/page/delete-completely-user-home-by-system';
+
 const logger = loggerFactory('growi:events:user');
 
 class UserEvent extends EventEmitter {
@@ -30,7 +32,7 @@ class UserEvent extends EventEmitter {
       // Since the type of page.creator is 'any', we resort to the following comparison,
       // checking if page.creator.toString() is not equal to user._id.toString(). Our code covers null, string, or object types.
       if (page != null && page.creator != null && page.creator.toString() !== user._id.toString()) {
-        await this.crowi.pageService.deleteCompletelyUserHomeBySystem(userHomepagePath);
+        await deleteCompletelyUserHomeBySystem(userHomepagePath, this.crowi.pageService);
         page = null;
       }
 

+ 2 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -8,6 +8,7 @@ import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { configManager } from '~/server/service/config-manager';
+import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -819,7 +820,7 @@ module.exports = (crowi) => {
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
       if (isUsersHomepageDeletionEnabled && isForceDeleteUserHomepageOnUserDeletion) {
-        crowi.pageService.deleteCompletelyUserHomeBySystem(homepagePath);
+        deleteCompletelyUserHomeBySystem(homepagePath, crowi.pageService);
       }
 
       return res.apiv3({ user: serializedUser });

+ 116 - 0
apps/app/src/server/service/page/delete-completely-user-home-by-system.ts

@@ -0,0 +1,116 @@
+import { Writable } from 'stream';
+
+import { getIdForRef } from '@growi/core';
+import type { IPage } from '@growi/core';
+import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
+import mongoose from 'mongoose';
+import streamToPromise from 'stream-to-promise';
+
+import type { PageModel } from '~/server/models/page';
+import { createBatchStream } from '~/server/util/batch-stream';
+import loggerFactory from '~/utils/logger';
+
+import { shouldUseV4Process } from './should-use-v4-process';
+
+import PageService, { BULK_REINDEX_SIZE } from '.';
+
+const logger = loggerFactory('growi:services:page');
+
+/**
+   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
+   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
+   *
+   * @param {string} userHomepagePath - The path of the user's homepage.
+   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
+   * @throws {Error} - If an error occurs during the deletion process.
+   */
+export const deleteCompletelyUserHomeBySystem = async(userHomepagePath: string, pageService: PageService): Promise<void> => {
+  if (!isUsersHomepage(userHomepagePath)) {
+    const msg = 'input value is not user homepage path.';
+    logger.error(msg);
+    throw new Error(msg);
+  }
+
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const userHomepage = await Page.findByPath(userHomepagePath, true);
+
+  if (userHomepage == null) {
+    const msg = 'user homepage is not found.';
+    logger.error(msg);
+    throw new Error(msg);
+  }
+
+  const isShouldUseV4Process = shouldUseV4Process(userHomepage);
+
+  const ids = [userHomepage._id];
+  const paths = [userHomepage.path];
+  const parentId = getIdForRef(userHomepage.parent);
+
+  try {
+    if (!isShouldUseV4Process) {
+      // Ensure consistency of ancestors
+      const inc = userHomepage.isEmpty ? -userHomepage.descendantCount : -(userHomepage.descendantCount + 1);
+      await pageService.updateDescendantCountOfAncestors(parentId, inc, true);
+    }
+
+    // Delete the user's homepage
+    await pageService.deleteCompletelyOperation(ids, paths);
+
+    if (!isShouldUseV4Process) {
+      // Remove leaf empty pages
+      await Page.removeLeafEmptyPagesRecursively(parentId);
+    }
+
+    if (!userHomepage.isEmpty) {
+      // Emit an event for the search service
+      pageService.pageEvent.emit('deleteCompletely', userHomepage);
+    }
+
+    const { PageQueryBuilder } = Page;
+
+    // Find descendant pages with system deletion condition
+    const builder = new PageQueryBuilder(Page.find(), true)
+      .addConditionForSystemDeletion()
+      .addConditionToListOnlyDescendants(userHomepage.path, {});
+
+    // Stream processing to delete descendant pages
+    // ────────┤ start │─────────
+    const readStream = await builder
+      .query
+      .lean()
+      .cursor({ batchSize: BULK_REINDEX_SIZE });
+
+    let count = 0;
+
+    const writeStream = new Writable({
+      objectMode: true,
+      async write(batch, encoding, callback) {
+        try {
+          count += batch.length;
+          // Delete multiple pages completely
+          await pageService.deleteMultipleCompletely(batch, undefined, {});
+          logger.debug(`Adding pages progressing: (count=${count})`);
+        }
+        catch (err) {
+          logger.error('addAllPages error on add anyway: ', err);
+        }
+        callback();
+      },
+      final(callback) {
+        logger.debug(`Adding pages has completed: (totalCount=${count})`);
+        callback();
+      },
+    });
+
+    readStream
+      .pipe(createBatchStream(BULK_REINDEX_SIZE))
+      .pipe(writeStream);
+
+    await streamToPromise(writeStream);
+    // ────────┤ end │─────────
+  }
+  catch (err) {
+    logger.error('Error occurred while deleting user homepage and subpages.', err);
+    throw err;
+  }
+};

+ 27 - 140
apps/app/src/server/service/page.ts → apps/app/src/server/service/page/index.ts

@@ -5,7 +5,7 @@ import type {
   Ref, HasObjectId, IUserHasId, IUser,
   IPage, IPageInfo, IPageInfoAll, IPageInfoForEntity, IPageWithMeta, IGrantedGroup,
 } from '@growi/core';
-import { PageGrant, PageStatus, getIdForRef } from '@growi/core';
+import { PageGrant, PageStatus } from '@growi/core';
 import {
   pagePathUtils, pathUtils,
 } from '@growi/core/dist/utils';
@@ -31,20 +31,21 @@ import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
-import { ObjectIdLike } from '../interfaces/mongoose-utils';
-import { Attachment } from '../models';
-import { PathAlreadyExistsError } from '../models/errors';
-import { IOptionsForCreate, IOptionsForUpdate } from '../models/interfaces/page-operation';
-import PageOperation, { PageOperationDocument } from '../models/page-operation';
-import { PageRedirectModel } from '../models/page-redirect';
-import { serializePageSecurely } from '../models/serializers/page-serializer';
-import ShareLink from '../models/share-link';
-import Subscription from '../models/subscription';
-import UserGroupRelation from '../models/user-group-relation';
-import { V5ConversionError } from '../models/vo/v5-conversion-error';
-import { divideByType } from '../util/granted-group';
-
-import { configManager } from './config-manager';
+import { ObjectIdLike } from '../../interfaces/mongoose-utils';
+import { Attachment } from '../../models';
+import { PathAlreadyExistsError } from '../../models/errors';
+import { IOptionsForCreate, IOptionsForUpdate } from '../../models/interfaces/page-operation';
+import PageOperation, { PageOperationDocument } from '../../models/page-operation';
+import { PageRedirectModel } from '../../models/page-redirect';
+import { serializePageSecurely } from '../../models/serializers/page-serializer';
+import ShareLink from '../../models/share-link';
+import Subscription from '../../models/subscription';
+import UserGroupRelation from '../../models/user-group-relation';
+import { V5ConversionError } from '../../models/vo/v5-conversion-error';
+import { divideByType } from '../../util/granted-group';
+import { configManager } from '../config-manager';
+
+import { shouldUseV4Process } from './should-use-v4-process';
 
 const debug = require('debug')('growi:services:page');
 
@@ -56,7 +57,7 @@ const {
 
 const { addTrailingSlash } = pathUtils;
 
-const BULK_REINDEX_SIZE = 100;
+export const BULK_REINDEX_SIZE = 100;
 const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
 
 // TODO: improve type
@@ -371,20 +372,6 @@ class PageService {
     };
   }
 
-  private shouldUseV4Process(page): boolean {
-    const Page = mongoose.model('Page') as unknown as PageModel;
-
-    const isTrashPage = page.status === Page.STATUS_DELETED;
-    const isPageMigrated = page.parent != null;
-    const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
-    const isRoot = isTopPage(page.path);
-    const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
-
-    const shouldUseV4Process = !isRoot && (!isV5Compatible || !isPageMigrated || isTrashPage || isPageRestricted);
-
-    return shouldUseV4Process;
-  }
-
   private shouldUseV4ProcessForRevert(page): boolean {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
@@ -453,8 +440,8 @@ class PageService {
     }
 
     // Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.renamePageV4(page, newPagePath, user, options);
     }
 
@@ -1014,8 +1001,8 @@ class PageService {
     newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // 1. Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.duplicateV4(page, newPagePath, user, isRecursively);
     }
 
@@ -1439,8 +1426,8 @@ class PageService {
     const Page = mongoose.model('Page') as PageModel;
 
     // Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.deletePageV4(page, user, options, isRecursively);
     }
     // Validate
@@ -1761,7 +1748,7 @@ class PageService {
     return nDeletedNonEmptyPages;
   }
 
-  private async deleteCompletelyOperation(pageIds, pagePaths) {
+  async deleteCompletelyOperation(pageIds, pagePaths) {
     // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
     const Bookmark = this.crowi.model('Bookmark');
     const Comment = this.crowi.model('Comment');
@@ -1786,7 +1773,7 @@ class PageService {
   }
 
   // delete multiple pages
-  private async deleteMultipleCompletely(pages, user, options = {}) {
+  async deleteMultipleCompletely(pages, user, options = {}) {
     const ids = pages.map(page => (page._id));
     const paths = pages.map(page => (page.path));
 
@@ -1814,8 +1801,8 @@ class PageService {
     }
 
     // v4 compatible process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.deleteCompletelyV4(page, user, options, isRecursively, preventEmitting);
     }
 
@@ -2038,106 +2025,6 @@ class PageService {
     }
   }
 
-  /**
-   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
-   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
-   *
-   * @param {string} userHomepagePath - The path of the user's homepage.
-   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
-   * @throws {Error} - If an error occurs during the deletion process.
-   */
-  async deleteCompletelyUserHomeBySystem(userHomepagePath: string): Promise<void> {
-    if (!isUsersHomepage(userHomepagePath)) {
-      const msg = 'input value is not user homepage path.';
-      logger.error(msg);
-      throw new Error(msg);
-    }
-
-    const Page = mongoose.model<IPage, PageModel>('Page');
-    const userHomepage = await Page.findByPath(userHomepagePath, true);
-
-    if (userHomepage == null) {
-      const msg = 'user homepage is not found.';
-      logger.error(msg);
-      throw new Error(msg);
-    }
-
-    const shouldUseV4Process = this.shouldUseV4Process(userHomepage);
-
-    const ids = [userHomepage._id];
-    const paths = [userHomepage.path];
-    const parentId = getIdForRef(userHomepage.parent);
-
-    try {
-      if (!shouldUseV4Process) {
-        // Ensure consistency of ancestors
-        const inc = userHomepage.isEmpty ? -userHomepage.descendantCount : -(userHomepage.descendantCount + 1);
-        await this.updateDescendantCountOfAncestors(parentId, inc, true);
-      }
-
-      // Delete the user's homepage
-      await this.deleteCompletelyOperation(ids, paths);
-
-      if (!shouldUseV4Process) {
-        // Remove leaf empty pages
-        await Page.removeLeafEmptyPagesRecursively(parentId);
-      }
-
-      if (!userHomepage.isEmpty) {
-        // Emit an event for the search service
-        this.pageEvent.emit('deleteCompletely', userHomepage);
-      }
-
-      const { PageQueryBuilder } = Page;
-
-      // Find descendant pages with system deletion condition
-      const builder = new PageQueryBuilder(Page.find(), true)
-        .addConditionForSystemDeletion()
-        .addConditionToListOnlyDescendants(userHomepage.path, {});
-
-      // Stream processing to delete descendant pages
-      // ────────┤ start │─────────
-      const readStream = await builder
-        .query
-        .lean()
-        .cursor({ batchSize: BULK_REINDEX_SIZE });
-
-      let count = 0;
-
-      const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
-      const writeStream = new Writable({
-        objectMode: true,
-        async write(batch, encoding, callback) {
-          try {
-            count += batch.length;
-            // Delete multiple pages completely
-            await deleteMultipleCompletely(batch, null, {});
-            logger.debug(`Adding pages progressing: (count=${count})`);
-          }
-          catch (err) {
-            logger.error('addAllPages error on add anyway: ', err);
-          }
-          callback();
-        },
-        final(callback) {
-          logger.debug(`Adding pages has completed: (totalCount=${count})`);
-          callback();
-        },
-      });
-
-      readStream
-        .pipe(createBatchStream(BULK_REINDEX_SIZE))
-        .pipe(writeStream);
-
-      await streamToPromise(writeStream);
-      // ────────┤ end │─────────
-    }
-    catch (err) {
-      logger.error('Error occurred while deleting user homepage and subpages.', err);
-      throw err;
-    }
-  }
-
   // use the same process in both v4 and v5
   private async revertDeletedDescendants(pages, user) {
     const Page = this.crowi.model('Page');

+ 20 - 0
apps/app/src/server/service/page/should-use-v4-process.ts

@@ -0,0 +1,20 @@
+import type { IPage } from '@growi/core';
+import { isTopPage } from '@growi/core/dist/utils/page-path-utils';
+import mongoose from 'mongoose';
+
+import { PageModel } from '~/server/models/page';
+import { configManager } from '~/server/service/config-manager';
+
+export const shouldUseV4Process = (page: IPage): boolean => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+
+  const isTrashPage = page.status === Page.STATUS_DELETED;
+  const isPageMigrated = page.parent != null;
+  const isV5Compatible = configManager.getConfig('crowi', 'app:isV5Compatible');
+  const isRoot = isTopPage(page.path);
+  const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
+
+  const shouldUseV4Process = !isRoot && (!isV5Compatible || !isPageMigrated || isTrashPage || isPageRestricted);
+
+  return shouldUseV4Process;
+};