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

fix subscription creation for PageBulkExport Job

Futa Arai 1 год назад
Родитель
Сommit
d94f9bd5e1

+ 8 - 2
apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts

@@ -39,8 +39,14 @@ module.exports = (crowi: Crowi): Router => {
       endpoint: req.originalUrl,
       endpoint: req.originalUrl,
     };
     };
 
 
-    pageBulkExportService?.bulkExportWithBasePagePath(path, req.user, activityParameters);
-    return res.apiv3({}, 204);
+    try {
+      await pageBulkExportService?.bulkExportWithBasePagePath(path, req.user, activityParameters);
+      return res.apiv3({}, 204);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Failed to start bulk export'));
+    }
   });
   });
 
 
   return router;
   return router;

+ 26 - 20
apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts

@@ -1,6 +1,7 @@
 import type { Readable } from 'stream';
 import type { Readable } from 'stream';
 import { Writable, pipeline } from 'stream';
 import { Writable, pipeline } from 'stream';
 
 
+import type { HasObjectId } from '@growi/core';
 import { type IPage, isPopulated, SubscriptionStatusType } from '@growi/core';
 import { type IPage, isPopulated, SubscriptionStatusType } from '@growi/core';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { Archiver } from 'archiver';
 import type { Archiver } from 'archiver';
@@ -54,8 +55,7 @@ class PageBulkExportService {
     const basePage = await Page.findByPathAndViewer(basePagePath, currentUser, null, true);
     const basePage = await Page.findByPathAndViewer(basePagePath, currentUser, null, true);
 
 
     if (basePage == null) {
     if (basePage == null) {
-      this.handleExportError(new Error('Base page not found or not accessible'));
-      return;
+      throw new Error('Base page not found or not accessible');
     }
     }
 
 
     const timeStamp = (new Date()).getTime();
     const timeStamp = (new Date()).getTime();
@@ -65,45 +65,55 @@ class PageBulkExportService {
     const zipArchiver = this.setUpZipArchiver();
     const zipArchiver = this.setUpZipArchiver();
     const pagesWritable = this.getPageWritable(zipArchiver);
     const pagesWritable = this.getPageWritable(zipArchiver);
     const bufferToPartSizeTransform = getBufferToFixedSizeTransform(this.partSize);
     const bufferToPartSizeTransform = getBufferToFixedSizeTransform(this.partSize);
-    let multipartUploadWritable: Writable;
 
 
     // init multipart upload
     // init multipart upload
     // TODO: Create abstract interface IMultipartUploader in https://redmine.weseek.co.jp/issues/135775
     // TODO: Create abstract interface IMultipartUploader in https://redmine.weseek.co.jp/issues/135775
     const multipartUploader: IAwsMultipartUploader | undefined = this.crowi?.fileUploadService?.createMultipartUploader(uploadKey);
     const multipartUploader: IAwsMultipartUploader | undefined = this.crowi?.fileUploadService?.createMultipartUploader(uploadKey);
+    let pageBulkExportJob: PageBulkExportJobDocument & HasObjectId;
+    if (multipartUploader == null) {
+      throw Error('Multipart upload not available for configured file upload type');
+    }
     try {
     try {
-      if (multipartUploader == null) {
-        throw Error('Multipart upload not available for configured file upload type');
-      }
       await multipartUploader.initUpload();
       await multipartUploader.initUpload();
-
-      const pageBulkExportJob = await PageBulkExportJob.create({
+      pageBulkExportJob = await PageBulkExportJob.create({
         user: currentUser._id,
         user: currentUser._id,
         page: basePage._id,
         page: basePage._id,
         uploadId: multipartUploader.uploadId,
         uploadId: multipartUploader.uploadId,
         format: PageBulkExportFormat.markdown,
         format: PageBulkExportFormat.markdown,
       });
       });
       await Subscription.upsertSubscription(currentUser, SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB, pageBulkExportJob, SubscriptionStatusType.SUBSCRIBE);
       await Subscription.upsertSubscription(currentUser, SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB, pageBulkExportJob, SubscriptionStatusType.SUBSCRIBE);
-
-      multipartUploadWritable = this.getMultipartUploadWritable(multipartUploader, pageBulkExportJob, currentUser, activityParameters);
     }
     }
     catch (err) {
     catch (err) {
-      await this.handleExportError(err, multipartUploader);
-      return;
+      await multipartUploader.abortUpload();
+      throw err;
     }
     }
 
 
+    const multipartUploadWritable = this.getMultipartUploadWritable(multipartUploader, pageBulkExportJob, currentUser, activityParameters);
+
     // Cannot directly pipe from pagesWritable to zipArchiver due to how the 'append' method works.
     // Cannot directly pipe from pagesWritable to zipArchiver due to how the 'append' method works.
     // Hence, execution of two pipelines is required.
     // Hence, execution of two pipelines is required.
-    pipeline(pagesReadable, pagesWritable, err => this.handleExportError(err, multipartUploader));
-    pipeline(zipArchiver, bufferToPartSizeTransform, multipartUploadWritable, err => this.handleExportError(err, multipartUploader));
+    pipeline(pagesReadable, pagesWritable, err => this.handleExportError(err, activityParameters, currentUser, pageBulkExportJob, multipartUploader));
+    pipeline(zipArchiver, bufferToPartSizeTransform, multipartUploadWritable,
+      err => this.handleExportError(err, activityParameters, currentUser, pageBulkExportJob, multipartUploader));
   }
   }
 
 
-  async handleExportError(err: Error | null, multipartUploader?: IAwsMultipartUploader): Promise<void> {
+  private async handleExportError(
+      err: Error | null, activityParameters: ActivityParameters, user, pageBulkExportJob: PageBulkExportJobDocument, multipartUploader?: IAwsMultipartUploader,
+  ): Promise<void> {
     if (err != null) {
     if (err != null) {
       logger.error(err);
       logger.error(err);
       if (multipartUploader != null) {
       if (multipartUploader != null) {
         await multipartUploader.abortUpload();
         await multipartUploader.abortUpload();
       }
       }
-      // TODO: notify failure to client: https://redmine.weseek.co.jp/issues/78037
+
+      const activity = await this.crowi.activityService.createActivity({
+        ...activityParameters,
+        action: SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED,
+        targetModel: SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB,
+        target: pageBulkExportJob,
+      });
+      const preNotify = preNotifyService.generatePreNotify(activity);
+      this.activityEvent.emit('updated', activity, pageBulkExportJob, preNotify);
     }
     }
   }
   }
 
 
@@ -204,12 +214,8 @@ class PageBulkExportService {
           const activity = await this.crowi.activityService.createActivity({
           const activity = await this.crowi.activityService.createActivity({
             ...activityParameters,
             ...activityParameters,
             action: SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED,
             action: SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED,
-            user,
             targetModel: SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB,
             targetModel: SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB,
             target: pageBulkExportJob,
             target: pageBulkExportJob,
-            snapshot: {
-              username: user.username,
-            },
           });
           });
           const preNotify = preNotifyService.generatePreNotify(activity);
           const preNotify = preNotifyService.generatePreNotify(activity);
           this.activityEvent.emit('updated', activity, pageBulkExportJob, preNotify);
           this.activityEvent.emit('updated', activity, pageBulkExportJob, preNotify);

+ 4 - 1
apps/app/src/interfaces/activity.ts

@@ -52,7 +52,8 @@ const ACTION_PAGE_RECURSIVELY_REVERT = 'PAGE_RECURSIVELY_REVERT';
 const ACTION_PAGE_SUBSCRIBE = 'PAGE_SUBSCRIBE';
 const ACTION_PAGE_SUBSCRIBE = 'PAGE_SUBSCRIBE';
 const ACTION_PAGE_UNSUBSCRIBE = 'PAGE_UNSUBSCRIBE';
 const ACTION_PAGE_UNSUBSCRIBE = 'PAGE_UNSUBSCRIBE';
 const ACTION_PAGE_EXPORT = 'PAGE_EXPORT';
 const ACTION_PAGE_EXPORT = 'PAGE_EXPORT';
-const ACTION_PAGE_BULK_EXPORT_COMPLETED = 'ACTION_PAGE_BULK_EXPORT_COMPLETED';
+const ACTION_PAGE_BULK_EXPORT_COMPLETED = 'PAGE_BULK_EXPORT_COMPLETED';
+const ACTION_PAGE_BULK_EXPORT_FAILED = 'PAGE_BULK_EXPORT_FAILED';
 const ACTION_TAG_UPDATE = 'TAG_UPDATE';
 const ACTION_TAG_UPDATE = 'TAG_UPDATE';
 const ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN = 'IN_APP_NOTIFICATION_ALL_STATUSES_OPEN';
 const ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN = 'IN_APP_NOTIFICATION_ALL_STATUSES_OPEN';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
@@ -344,6 +345,7 @@ export const SupportedAction = {
   ACTION_ADMIN_SEARCH_INDICES_NORMALIZE,
   ACTION_ADMIN_SEARCH_INDICES_NORMALIZE,
   ACTION_ADMIN_SEARCH_INDICES_REBUILD,
   ACTION_ADMIN_SEARCH_INDICES_REBUILD,
   ACTION_PAGE_BULK_EXPORT_COMPLETED,
   ACTION_PAGE_BULK_EXPORT_COMPLETED,
+  ACTION_PAGE_BULK_EXPORT_FAILED,
 } as const;
 } as const;
 
 
 // Action required for notification
 // Action required for notification
@@ -363,6 +365,7 @@ export const EssentialActionGroup = {
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_CREATE,
   ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
   ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
   ACTION_PAGE_BULK_EXPORT_COMPLETED,
   ACTION_PAGE_BULK_EXPORT_COMPLETED,
+  ACTION_PAGE_BULK_EXPORT_FAILED,
 } as const;
 } as const;
 
 
 export const ActionGroupSize = {
 export const ActionGroupSize = {

+ 11 - 7
apps/app/src/server/models/activity.ts

@@ -1,13 +1,17 @@
 import type { Ref, IPage } from '@growi/core';
 import type { Ref, IPage } from '@growi/core';
-import {
-  Types, Document, Model, Schema, SortOrder,
+import type {
+  Types, Document, Model, SortOrder,
 } from 'mongoose';
 } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 
+import type {
+  IActivity, ISnapshot, SupportedActionType, SupportedTargetModelType, SupportedEventModelType,
+} from '~/interfaces/activity';
 import {
 import {
-  IActivity, ISnapshot, AllSupportedActions, SupportedActionType,
-  AllSupportedTargetModels, SupportedTargetModelType,
-  AllSupportedEventModels, SupportedEventModelType,
+  AllSupportedActions,
+  AllSupportedTargetModels,
+  AllSupportedEventModels,
 } from '~/interfaces/activity';
 } from '~/interfaces/activity';
 
 
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
@@ -21,7 +25,7 @@ const logger = loggerFactory('growi:models:activity');
 
 
 export interface ActivityDocument extends Document {
 export interface ActivityDocument extends Document {
   _id: Types.ObjectId
   _id: Types.ObjectId
-  user: Types.ObjectId
+  user?: Types.ObjectId
   ip: string
   ip: string
   endpoint: string
   endpoint: string
   targetModel: SupportedTargetModelType
   targetModel: SupportedTargetModelType
@@ -29,7 +33,7 @@ export interface ActivityDocument extends Document {
   eventModel: SupportedEventModelType
   eventModel: SupportedEventModelType
   event: Types.ObjectId
   event: Types.ObjectId
   action: SupportedActionType
   action: SupportedActionType
-  snapshot: ISnapshot
+  snapshot?: ISnapshot
 
 
   getNotificationTargetUsers(): Promise<any[]>
   getNotificationTargetUsers(): Promise<any[]>
 }
 }

+ 2 - 2
apps/app/src/server/service/pre-notify.ts

@@ -2,7 +2,7 @@ import type {
   IPage, IUser, Ref,
   IPage, IUser, Ref,
 } from '@growi/core';
 } from '@growi/core';
 
 
-import { ActivityDocument } from '../models/activity';
+import type { ActivityDocument } from '../models/activity';
 import Subscription from '../models/subscription';
 import Subscription from '../models/subscription';
 import { getModelSafely } from '../util/mongoose-utils';
 import { getModelSafely } from '../util/mongoose-utils';
 
 
@@ -38,7 +38,7 @@ class PreNotifyService implements IPreNotifyService {
       const actionUser = activity.user;
       const actionUser = activity.user;
       const target = activity.target;
       const target = activity.target;
       const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);
       const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);
-      const notificationUsers = subscribedUsers.filter(item => (item.toString() !== actionUser._id.toString()));
+      const notificationUsers = subscribedUsers.filter(item => (item.toString() !== actionUser?._id.toString()));
       const activeNotificationUsers = await User.find({
       const activeNotificationUsers = await User.find({
         _id: { $in: notificationUsers },
         _id: { $in: notificationUsers },
         status: User.STATUS_ACTIVE,
         status: User.STATUS_ACTIVE,