|
|
@@ -1,20 +1,24 @@
|
|
|
+import fs from 'fs';
|
|
|
+import path from 'path';
|
|
|
+import { Readable, Transform } from 'stream';
|
|
|
+
|
|
|
import { toArrayIfNot } from '~/utils/array-utils';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
+import CollectionProgress from '../models/vo/collection-progress';
|
|
|
+import CollectionProgressingStatus from '../models/vo/collection-progressing-status';
|
|
|
+
|
|
|
+import AppService from './app';
|
|
|
import ConfigLoader from './config-loader';
|
|
|
+import GrowiBridgeService, { ZipFileStat } from './growi-bridge';
|
|
|
|
|
|
-const logger = loggerFactory('growi:services:ExportService'); // eslint-disable-line no-unused-vars
|
|
|
|
|
|
-const fs = require('fs');
|
|
|
-const path = require('path');
|
|
|
-const { Transform } = require('stream');
|
|
|
+const logger = loggerFactory('growi:services:ExportService'); // eslint-disable-line no-unused-vars
|
|
|
|
|
|
const archiver = require('archiver');
|
|
|
const mongoose = require('mongoose');
|
|
|
const streamToPromise = require('stream-to-promise');
|
|
|
|
|
|
-const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
|
|
|
-
|
|
|
class ExportProgressingStatus extends CollectionProgressingStatus {
|
|
|
|
|
|
async init() {
|
|
|
@@ -32,14 +36,30 @@ class ExportProgressingStatus extends CollectionProgressingStatus {
|
|
|
|
|
|
class ExportService {
|
|
|
|
|
|
+ crowi: any;
|
|
|
+
|
|
|
+ appService: AppService;
|
|
|
+
|
|
|
+ growiBridgeService: GrowiBridgeService;
|
|
|
+
|
|
|
+ getFile: (filename: string) => string;
|
|
|
+
|
|
|
+ per = 100;
|
|
|
+
|
|
|
+ zlibLevel = 9; // 0(min) - 9(max)
|
|
|
+
|
|
|
+ currentProgressingStatus: ExportProgressingStatus | null;
|
|
|
+
|
|
|
+ baseDir: string;
|
|
|
+
|
|
|
+ adminEvent: any;
|
|
|
+
|
|
|
constructor(crowi) {
|
|
|
this.crowi = crowi;
|
|
|
this.appService = crowi.appService;
|
|
|
this.growiBridgeService = crowi.growiBridgeService;
|
|
|
this.getFile = this.growiBridgeService.getFile.bind(this);
|
|
|
this.baseDir = path.join(crowi.tmpDir, 'downloads');
|
|
|
- this.per = 100;
|
|
|
- this.zlibLevel = 9; // 0(min) - 9(max)
|
|
|
|
|
|
this.adminEvent = crowi.event('admin');
|
|
|
|
|
|
@@ -56,7 +76,7 @@ class ExportService {
|
|
|
const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
|
|
|
|
|
|
// process serially so as not to waste memory
|
|
|
- const zipFileStats = [];
|
|
|
+ const zipFileStats: ZipFileStat[] = [];
|
|
|
const parseZipFilePromises = zipFiles.map((file) => {
|
|
|
const zipFile = this.getFile(file);
|
|
|
return this.growiBridgeService.parseZipFile(zipFile);
|
|
|
@@ -73,7 +93,7 @@ class ExportService {
|
|
|
return {
|
|
|
zipFileStats: filtered,
|
|
|
isExporting,
|
|
|
- progressList: isExporting ? this.currentProgressingStatus.progressList : null,
|
|
|
+ progressList: isExporting ? this.currentProgressingStatus?.progressList : null,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -83,7 +103,7 @@ class ExportService {
|
|
|
* @memberOf ExportService
|
|
|
* @return {string} path to meta.json
|
|
|
*/
|
|
|
- async createMetaJson() {
|
|
|
+ async createMetaJson(): Promise<string> {
|
|
|
const metaJson = path.join(this.baseDir, this.growiBridgeService.getMetaFileName());
|
|
|
const writeStream = fs.createWriteStream(metaJson, { encoding: this.growiBridgeService.getEncoding() });
|
|
|
const passwordSeed = this.crowi.env.PASSWORD_SEED || null;
|
|
|
@@ -109,7 +129,7 @@ class ExportService {
|
|
|
* @param {ExportProgress} exportProgress
|
|
|
* @return {Transform}
|
|
|
*/
|
|
|
- generateLogStream(exportProgress) {
|
|
|
+ generateLogStream(exportProgress: CollectionProgress | undefined): Transform {
|
|
|
const logProgress = this.logProgress.bind(this);
|
|
|
|
|
|
let count = 0;
|
|
|
@@ -130,9 +150,9 @@ class ExportService {
|
|
|
* insert beginning/ending brackets and comma separator for Json Array
|
|
|
*
|
|
|
* @memberOf ExportService
|
|
|
- * @return {TransformStream}
|
|
|
+ * @return {Transform}
|
|
|
*/
|
|
|
- generateTransformStream() {
|
|
|
+ generateTransformStream(): Transform {
|
|
|
let isFirst = true;
|
|
|
|
|
|
const transformStream = new Transform({
|
|
|
@@ -171,7 +191,7 @@ class ExportService {
|
|
|
* @param {string} collectionName collection name
|
|
|
* @return {string} path to zip file
|
|
|
*/
|
|
|
- async exportCollectionToJson(collectionName) {
|
|
|
+ async exportCollectionToJson(collectionName: string): Promise<string> {
|
|
|
const collection = mongoose.connection.collection(collectionName);
|
|
|
|
|
|
const nativeCursor = collection.find();
|
|
|
@@ -181,7 +201,7 @@ class ExportService {
|
|
|
const transformStream = this.generateTransformStream();
|
|
|
|
|
|
// log configuration
|
|
|
- const exportProgress = this.currentProgressingStatus.progressMap[collectionName];
|
|
|
+ const exportProgress = this.currentProgressingStatus?.progressMap[collectionName];
|
|
|
const logStream = this.generateLogStream(exportProgress);
|
|
|
|
|
|
// create WritableStream
|
|
|
@@ -195,7 +215,7 @@ class ExportService {
|
|
|
|
|
|
await streamToPromise(writeStream);
|
|
|
|
|
|
- return writeStream.path;
|
|
|
+ return writeStream.path.toString();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -203,13 +223,13 @@ class ExportService {
|
|
|
*
|
|
|
* @memberOf ExportService
|
|
|
* @param {Array.<string>} collections array of collection name
|
|
|
- * @return {Array.<string>} paths to json files created
|
|
|
+ * @return {Array.<ZipFileStat>} info of zip file created
|
|
|
*/
|
|
|
- async exportCollectionsToZippedJson(collections) {
|
|
|
+ async exportCollectionsToZippedJson(collections: string[]): Promise<ZipFileStat> {
|
|
|
const metaJson = await this.createMetaJson();
|
|
|
|
|
|
// process serially so as not to waste memory
|
|
|
- const jsonFiles = [];
|
|
|
+ const jsonFiles: string[] = [];
|
|
|
const jsonFilesPromises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
|
|
|
for await (const jsonFile of jsonFilesPromises) {
|
|
|
jsonFiles.push(jsonFile);
|
|
|
@@ -236,7 +256,7 @@ class ExportService {
|
|
|
// TODO: remove broken zip file
|
|
|
}
|
|
|
|
|
|
- async export(collections) {
|
|
|
+ async export(collections: string[]): Promise<ZipFileStat> {
|
|
|
if (this.currentProgressingStatus != null) {
|
|
|
throw new Error('There is an exporting process running.');
|
|
|
}
|
|
|
@@ -244,7 +264,7 @@ class ExportService {
|
|
|
this.currentProgressingStatus = new ExportProgressingStatus(collections);
|
|
|
await this.currentProgressingStatus.init();
|
|
|
|
|
|
- let zipFileStat;
|
|
|
+ let zipFileStat: ZipFileStat;
|
|
|
try {
|
|
|
zipFileStat = await this.exportCollectionsToZippedJson(collections);
|
|
|
}
|
|
|
@@ -263,7 +283,9 @@ class ExportService {
|
|
|
* @param {CollectionProgress} collectionProgress
|
|
|
* @param {number} currentCount number of items exported
|
|
|
*/
|
|
|
- logProgress(collectionProgress, currentCount) {
|
|
|
+ logProgress(collectionProgress: CollectionProgress | undefined, currentCount: number): void {
|
|
|
+ if (collectionProgress == null) return;
|
|
|
+
|
|
|
const output = `${collectionProgress.collectionName}: ${currentCount}/${collectionProgress.totalCount} written`;
|
|
|
|
|
|
// update exportProgress.currentCount
|
|
|
@@ -284,12 +306,11 @@ class ExportService {
|
|
|
/**
|
|
|
* emit progress event
|
|
|
*/
|
|
|
- emitProgressEvent() {
|
|
|
- const { currentCount, totalCount, progressList } = this.currentProgressingStatus;
|
|
|
+ emitProgressEvent(): void {
|
|
|
const data = {
|
|
|
- currentCount,
|
|
|
- totalCount,
|
|
|
- progressList,
|
|
|
+ currentCount: this.currentProgressingStatus?.currentCount,
|
|
|
+ totalCount: this.currentProgressingStatus?.totalCount,
|
|
|
+ progressList: this.currentProgressingStatus?.progressList,
|
|
|
};
|
|
|
|
|
|
// send event (in progress in global)
|
|
|
@@ -299,7 +320,7 @@ class ExportService {
|
|
|
/**
|
|
|
* emit start zipping event
|
|
|
*/
|
|
|
- emitStartZippingEvent() {
|
|
|
+ emitStartZippingEvent(): void {
|
|
|
this.adminEvent.emit('onStartZippingForExport', {});
|
|
|
}
|
|
|
|
|
|
@@ -307,7 +328,7 @@ class ExportService {
|
|
|
* emit terminate event
|
|
|
* @param {object} zipFileStat added zip file status data
|
|
|
*/
|
|
|
- emitTerminateEvent(zipFileStat) {
|
|
|
+ emitTerminateEvent(zipFileStat: ZipFileStat): void {
|
|
|
this.adminEvent.emit('onTerminateForExport', { addedZipFileStat: zipFileStat });
|
|
|
}
|
|
|
|
|
|
@@ -319,7 +340,7 @@ class ExportService {
|
|
|
* @return {string} absolute path to the zip file
|
|
|
* @see https://www.archiverjs.com/#quick-start
|
|
|
*/
|
|
|
- async zipFiles(_configs) {
|
|
|
+ async zipFiles(_configs: {from: string, as: string}[]): Promise<string> {
|
|
|
const configs = toArrayIfNot(_configs);
|
|
|
const appTitle = this.appService.getAppTitle();
|
|
|
const timeStamp = (new Date()).getTime();
|
|
|
@@ -365,10 +386,9 @@ class ExportService {
|
|
|
return zipFile;
|
|
|
}
|
|
|
|
|
|
- getReadStreamFromRevision(revision, format) {
|
|
|
+ getReadStreamFromRevision(revision, format): Readable {
|
|
|
const data = revision.body;
|
|
|
|
|
|
- const Readable = require('stream').Readable;
|
|
|
const readable = new Readable();
|
|
|
readable._read = () => {};
|
|
|
readable.push(data);
|
|
|
@@ -379,4 +399,4 @@ class ExportService {
|
|
|
|
|
|
}
|
|
|
|
|
|
-module.exports = ExportService;
|
|
|
+export default ExportService;
|