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

Merge pull request #8496 from weseek/feat/140684-automatic-deletion-of-wip-pages-using-ttl-index

feat: Automatic deletion of wip pages using TTL index
Shun Miyazawa 2 лет назад
Родитель
Сommit
0a4b7b30b4

+ 1 - 0
apps/app/src/server/crowi/index.js

@@ -725,6 +725,7 @@ Crowi.prototype.setupPageService = async function() {
   // initialize after pageGrantService since pageService uses pageGrantService in constructor
   if (this.pageService == null) {
     this.pageService = new PageService(this);
+    await this.pageService.createTtlIndex();
   }
   if (this.pageOperationService == null) {
     this.pageOperationService = new PageOperationService(this);

+ 8 - 5
apps/app/src/server/models/page.ts

@@ -140,7 +140,7 @@ const schema = new Schema<PageDocument, PageModel>({
   commentCount: { type: Number, default: 0 },
   expandContentWidth: { type: Boolean },
   wip: { type: Boolean },
-  wipExpiredAt: { type: Date },
+  ttlTimestamp: { type: Date, index: true },
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   deleteUser: { type: ObjectId, ref: 'User' },
   deletedAt: { type: Date },
@@ -1055,17 +1055,20 @@ schema.methods.calculateAndUpdateLatestRevisionBodyLength = async function(this:
 
 schema.methods.publish = function() {
   this.wip = undefined;
-  this.wipExpiredAt = undefined;
+  this.ttlTimestamp = undefined;
 };
 
 schema.methods.unpublish = function() {
   this.wip = true;
-  this.wipExpiredAt = undefined;
+  this.ttlTimestamp = undefined;
 };
 
-schema.methods.makeWip = function() {
+schema.methods.makeWip = function(disableTtl: boolean) {
   this.wip = true;
-  this.wipExpiredAt = new Date();
+
+  if (!disableTtl) {
+    this.ttlTimestamp = new Date();
+  }
 };
 
 /*

+ 8 - 3
apps/app/src/server/service/config-loader.ts

@@ -4,9 +4,8 @@ import { parseISO } from 'date-fns';
 import { GrowiServiceType } from '~/features/questionnaire/interfaces/growi-info';
 import loggerFactory from '~/utils/logger';
 
-import ConfigModel, {
-  Config, defaultCrowiConfigs, defaultMarkdownConfigs, defaultNotificationConfigs,
-} from '../models/config';
+import type { Config } from '../models/config';
+import ConfigModel, { defaultCrowiConfigs, defaultMarkdownConfigs, defaultNotificationConfigs } from '../models/config';
 
 
 const logger = loggerFactory('growi:service:ConfigLoader');
@@ -712,6 +711,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type: ValueType.NUMBER,
     default: 30000,
   },
+  WIP_PAGE_EXPIRATION_SECONDS: {
+    ns: 'crowi',
+    key: 'app:wipPageExpirationSeconds',
+    type: ValueType.NUMBER,
+    default: 172800, // 2 days
+  },
 };
 
 

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

@@ -584,6 +584,8 @@ class PageService implements IPageService {
 
       this.activityEvent.emit('updated', activity, page, preNotify);
     }
+
+    this.disableAncestorPagesTtl(newPagePath);
     return renamedPage;
   }
 
@@ -3812,9 +3814,10 @@ class PageService implements IPageService {
       page.parent = parent._id;
     }
 
-    // Set wip
+    // Make WIP
     if (options.wip) {
-      page.makeWip();
+      const hasChildren = await Page.exists({ parent: page._id });
+      page.makeWip(hasChildren != null); // disableTtl = hasChildren != null
     }
 
     // Save
@@ -3855,6 +3858,8 @@ class PageService implements IPageService {
    * Used to run sub operation in create method
    */
   async createSubOperation(page, user, options: IOptionsForCreate, pageOpId: ObjectIdLike): Promise<void> {
+    await this.disableAncestorPagesTtl(page.path);
+
     // Update descendantCount
     await this.updateDescendantCountOfAncestors(page._id, 1, false);
 
@@ -3939,6 +3944,18 @@ class PageService implements IPageService {
     return this.canProcessCreate(path, grantData, false);
   }
 
+  private async disableAncestorPagesTtl(path: string): Promise<void> {
+    const Page = mongoose.model<PageDocument, PageModel>('Page');
+
+    const ancestorPaths = collectAncestorPaths(path);
+    const ancestorPageIds = await Page.aggregate([
+      { $match: { path: { $in: ancestorPaths, $nin: ['/'] }, isEmpty: false } },
+      { $project: { _id: 1 } },
+    ]);
+
+    await Page.updateMany({ _id: { $in: ancestorPageIds } }, { $unset: { ttlTimestamp: true } });
+  }
+
   /**
    * @private
    * This method receives the same arguments as the PageService.create method does except for the added type '{ grantUserIds?: ObjectIdLike[] }'.
@@ -4413,6 +4430,34 @@ class PageService implements IPageService {
     });
   }
 
+  async createTtlIndex(): Promise<void> {
+    const wipPageExpirationSeconds = configManager.getConfig('crowi', 'app:wipPageExpirationSeconds') ?? 172800;
+    const collection = mongoose.connection.collection('pages');
+
+    try {
+      const targetField = 'ttlTimestamp_1';
+
+      const indexes = await collection.indexes();
+      const foundTargetField = indexes.find(i => i.name === targetField);
+
+      const isNotSpec = foundTargetField?.expireAfterSeconds == null || foundTargetField?.expireAfterSeconds !== wipPageExpirationSeconds;
+      const shoudDropIndex = foundTargetField != null && isNotSpec;
+      const shoudCreateIndex = foundTargetField == null || shoudDropIndex;
+
+      if (shoudDropIndex) {
+        await collection.dropIndex(targetField);
+      }
+
+      if (shoudCreateIndex) {
+        await collection.createIndex({ ttlTimestamp: 1 }, { expireAfterSeconds: wipPageExpirationSeconds });
+      }
+    }
+    catch (err) {
+      logger.error('Failed to create TTL Index', err);
+      throw err;
+    }
+  }
+
 }
 
 export default PageService;

+ 1 - 1
packages/core/src/interfaces/page.ts

@@ -40,7 +40,7 @@ export type IPage = {
   latestRevisionBodyLength?: number,
   expandContentWidth?: boolean,
   wip?: boolean,
-  wipExpiredAt?: Date
+  ttlTimestamp?: Date
 }
 
 export type IPagePopulatedToList = Omit<IPageHasId, 'lastUpdateUser'> & {