Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/158222 158279 bulk export for file upload types other than s3/gcs #9467

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/app/public/static/locales/en_US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,6 @@
"bulk_export_started": "Please wait a moment...",
"bulk_export_download_expired": "Download period has expired",
"bulk_export_job_expired": "Export process was canceled because it took too long",
"bulk_export_only_available_for": "Only available for AWS or GCP",
"export_in_progress": "Export in progress",
"export_in_progress_explanation": "Export with the same format is already in progress. Would you like to restart to export the latest page contents?",
"export_cancel_warning": "The following export in progress will be canceled",
Expand Down
1 change: 0 additions & 1 deletion apps/app/public/static/locales/fr_FR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,6 @@
"bulk_export_started": "Patientez s'il-vous-plait...",
"bulk_export_download_expired": "La période de téléchargement a expiré",
"bulk_export_job_expired": "Le traitement a été interrompu car le temps d'exportation était trop long",
"bulk_export_only_available_for": "Uniquement disponible pour AWS ou GCP",
"export_in_progress": "Exportation en cours",
"export_in_progress_explanation": "L'exportation avec le même format est déjà en cours. Souhaitez-vous redémarrer pour exporter le dernier contenu de la page ?",
"export_cancel_warning": "Les exportations suivantes en cours seront annulées",
Expand Down
1 change: 0 additions & 1 deletion apps/app/public/static/locales/ja_JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,6 @@
"bulk_export_started": "ただいま準備中です...",
"bulk_export_download_expired": "ダウンロード期限が切れました",
"bulk_export_job_expired": "エクスポート時間が長すぎるため、処理が中断されました",
"bulk_export_only_available_for": "AWS と GCP のみ対応しています",
"export_in_progress": "エクスポート進行中",
"export_in_progress_explanation": "既に同じ形式でのエクスポートが進行中です。最新のページ内容でエクスポートを最初からやり直しますか?",
"export_cancel_warning": "進行中の以下のエクスポートはキャンセルされます",
Expand Down
1 change: 0 additions & 1 deletion apps/app/public/static/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,6 @@
"bulk_export_started": "目前我们正在准备...",
"bulk_export_download_expired": "下载期限已过",
"bulk_export_job_expired": "由于导出时间太长,处理被中断",
"bulk_export_only_available_for": "仅适用于 AWS 或 GCP",
"export_in_progress": "导出正在进行中",
"export_in_progress_explanation": "已在进行相同格式的导出。您要重新启动以导出最新的页面内容吗?",
"export_cancel_warning": "以下正在进行的导出将被取消",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element

const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);

const [isBulkExportTooltipOpen, setIsBulkExportTooltipOpen] = useState(false);

const syncLatestRevisionBodyHandler = useCallback(async() => {
// eslint-disable-next-line no-alert
const answer = window.confirm(t('sync-latest-revision-body.confirm'));
Expand Down Expand Up @@ -144,25 +142,18 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
</DropdownItem>

{/* Bulk export */}
<span id="bulkExportDropdownItem">
<DropdownItem
disabled={!isPageBulkExportEnabled}
onClick={openPageBulkExportSelectModal}
className="grw-page-control-dropdown-item"
>
<span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
{t('page_export.bulk_export')}
</DropdownItem>
</span>
<Tooltip
placement="left"
isOpen={!isPageBulkExportEnabled && isBulkExportTooltipOpen}
// Tooltip cannot be activated when target is disabled so set the target to wrapper span
target="bulkExportDropdownItem"
toggle={() => setIsBulkExportTooltipOpen(!isBulkExportTooltipOpen)}
>
{t('page_export.bulk_export_only_available_for')}
</Tooltip>
{isPageBulkExportEnabled && (
<span id="bulkExportDropdownItem">
<DropdownItem
disabled={!isPageBulkExportEnabled}
onClick={openPageBulkExportSelectModal}
className="grw-page-control-dropdown-item"
>
<span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
{t('page_export.bulk_export')}
</DropdownItem>
</span>
)}

<DropdownItem divider />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ export interface IPageBulkExportJob {
user: Ref<IUser>, // user that started export job
page: Ref<IPage>, // the root page of page tree to export
lastExportedPagePath?: string, // the path of page that was exported to the fs last
uploadId?: string, // upload ID of multipart upload of S3/GCS
uploadKey?: string, // upload key of multipart upload of S3/GCS
format: PageBulkExportFormat,
completedAt?: Date, // the date at which job was completed
attachment?: Ref<IAttachment>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ const pageBulkExportJobSchema = new Schema<PageBulkExportJobDocument>({
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
page: { type: Schema.Types.ObjectId, ref: 'Page', required: true },
lastExportedPagePath: { type: String },
uploadId: { type: String, unique: true, sparse: true },
uploadKey: { type: String, unique: true, sparse: true },
format: { type: String, enum: Object.values(PageBulkExportFormat), required: true },
completedAt: { type: Date },
attachment: { type: Schema.Types.ObjectId, ref: 'Attachment' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class PageBulkExportJobCleanUpCronService extends CronService {
}

override async executeJob(): Promise<void> {
const isPageBulkExportEnabled = PageBulkExportEnabledFileUploadTypes.includes(configManager.getConfig('crowi', 'app:fileUploadType'));
// TODO: allow enabling/disabling bulk export in https://redmine.weseek.co.jp/issues/158221
const isPageBulkExportEnabled = true;
if (!isPageBulkExportEnabled) return;

await this.deleteExpiredExportJobs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
import type { ActivityDocument } from '~/server/models/activity';
import { configManager } from '~/server/service/config-manager';
import CronService from '~/server/service/cron';
import type { FileUploader } from '~/server/service/file-uploader';
import { preNotifyService } from '~/server/service/pre-notify';
import loggerFactory from '~/utils/logger';

Expand All @@ -24,7 +23,7 @@ import PageBulkExportPageSnapshot from '../../models/page-bulk-export-page-snaps


import { BulkExportJobExpiredError, BulkExportJobRestartedError } from './errors';
import { compressAndUploadAsync } from './steps/compress-and-upload-async';
import { compressAndUpload } from './steps/compress-and-upload';
import { createPageSnapshotsAsync } from './steps/create-page-snapshots-async';
import { exportPagesToFsAsync } from './steps/export-pages-to-fs-async';

Expand All @@ -37,7 +36,8 @@ export interface IPageBulkExportJobCronService {
maxPartSize: number;
compressExtension: string;
setStreamInExecution(jobId: ObjectIdLike, stream: Readable): void;
handlePipelineError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument): void;
removeStreamInExecution(jobId: ObjectIdLike): void;
handleError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument): void;
notifyExportResultAndCleanUp(action: SupportedActionType, pageBulkExportJob: PageBulkExportJobDocument): Promise<void>;
getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument): string;
}
Expand Down Expand Up @@ -157,7 +157,7 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
exportPagesToFsAsync.bind(this)(pageBulkExportJob);
}
else if (pageBulkExportJob.status === PageBulkExportJobStatus.uploading) {
await compressAndUploadAsync.bind(this)(user, pageBulkExportJob);
compressAndUpload.bind(this)(user, pageBulkExportJob);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multipart upload の準備のエラーをキャッチするために await していたが、multipart upload しなくなったため await を除外。
(また、await してしまうと、compressAndUpload 内の uploadAttachment を待つようになってしまう

}
}
catch (err) {
Expand All @@ -167,11 +167,11 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
}

/**
* Handle errors that occurred inside a stream pipeline
* Handle errors that occurred during page bulk export
* @param err error
* @param pageBulkExportJob PageBulkExportJob executed in the pipeline
*/
async handlePipelineError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument) {
async handleError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument) {
if (err == null) return;

if (err instanceof BulkExportJobExpiredError) {
Expand Down Expand Up @@ -215,7 +215,6 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
* Do the following in parallel:
* - delete page snapshots
* - remove the temporal output directory
* - abort multipart upload
*/
async cleanUpExportJobResources(pageBulkExportJob: PageBulkExportJobDocument, restarted = false) {
const streamInExecution = this.getStreamInExecution(pageBulkExportJob._id);
Expand All @@ -226,19 +225,14 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
else {
streamInExecution.destroy(new BulkExportJobExpiredError());
}
this.removeStreamInExecution(pageBulkExportJob._id);
}
this.removeStreamInExecution(pageBulkExportJob._id);

const promises = [
PageBulkExportPageSnapshot.deleteMany({ pageBulkExportJob }),
fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob), { recursive: true, force: true }),
];

const fileUploadService: FileUploader = this.crowi.fileUploadService;
if (pageBulkExportJob.uploadKey != null && pageBulkExportJob.uploadId != null) {
promises.push(fileUploadService.abortPreviousMultipartUpload(pageBulkExportJob.uploadKey, pageBulkExportJob.uploadId));
}

const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === 'rejected') logger.error(result.reason);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Archiver } from 'archiver';
import archiver from 'archiver';

import { PageBulkExportJobStatus } from '~/features/page-bulk-export/interfaces/page-bulk-export';
import { SupportedAction } from '~/interfaces/activity';
import { AttachmentType } from '~/server/interfaces/attachment';
import type { IAttachmentDocument } from '~/server/models/attachment';
import { Attachment } from '~/server/models/attachment';
import type { FileUploader } from '~/server/service/file-uploader';
import loggerFactory from '~/utils/logger';

import type { IPageBulkExportJobCronService } from '..';
import type { PageBulkExportJobDocument } from '../../../models/page-bulk-export-job';

const logger = loggerFactory('growi:service:page-bulk-export-job-cron:compress-and-upload-async');

function setUpPageArchiver(): Archiver {
const pageArchiver = archiver('tar', {
gzip: true,
});

// good practice to catch warnings (ie stat failures and other non-blocking errors)
pageArchiver.on('warning', (err) => {
if (err.code === 'ENOENT') logger.error(err);
else throw err;
});

return pageArchiver;
}

async function postProcess(
this: IPageBulkExportJobCronService, pageBulkExportJob: PageBulkExportJobDocument, attachment: IAttachmentDocument, fileSize: number,
): Promise<void> {
attachment.fileSize = fileSize;
await attachment.save();

pageBulkExportJob.completedAt = new Date();
pageBulkExportJob.attachment = attachment._id;
pageBulkExportJob.status = PageBulkExportJobStatus.completed;
await pageBulkExportJob.save();

this.removeStreamInExecution(pageBulkExportJob._id);
await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED, pageBulkExportJob);
}

/**
* Execute a pipeline that reads the page files from the temporal fs directory, compresses them, and uploads to the cloud storage
*/
export async function compressAndUpload(this: IPageBulkExportJobCronService, user, pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
const pageArchiver = setUpPageArchiver();

if (pageBulkExportJob.revisionListHash == null) throw new Error('revisionListHash is not set');
const originalName = `${pageBulkExportJob.revisionListHash}.${this.compressExtension}`;
const attachment = Attachment.createWithoutSave(null, user, originalName, this.compressExtension, 0, AttachmentType.PAGE_BULK_EXPORT);

const fileUploadService: FileUploader = this.crowi.fileUploadService;

pageArchiver.directory(this.getTmpOutputDir(pageBulkExportJob), false);
pageArchiver.finalize();
this.setStreamInExecution(pageBulkExportJob._id, pageArchiver);

try {
await fileUploadService.uploadAttachment(pageArchiver, attachment);
}
catch (e) {
logger.error(e);
this.handleError(e, pageBulkExportJob);
}
await postProcess.bind(this)(pageBulkExportJob, attachment, pageArchiver.pointer());
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ export async function createPageSnapshotsAsync(this: IPageBulkExportJobCronServi
this.setStreamInExecution(pageBulkExportJob._id, pagesReadable);

pipeline(pagesReadable, pageSnapshotsWritable, (err) => {
this.handlePipelineError(err, pageBulkExportJob);
this.handleError(err, pageBulkExportJob);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ export function exportPagesToFsAsync(this: IPageBulkExportJobCronService, pageBu
this.setStreamInExecution(pageBulkExportJob._id, pageSnapshotsReadable);

pipeline(pageSnapshotsReadable, pagesWritable, (err) => {
this.handlePipelineError(err, pageBulkExportJob);
this.handleError(err, pageBulkExportJob);
});
}
Loading
Loading