|
@@ -3,32 +3,49 @@ const fs = require('fs');
|
|
|
const path = require('path');
|
|
const path = require('path');
|
|
|
const streamToPromise = require('stream-to-promise');
|
|
const streamToPromise = require('stream-to-promise');
|
|
|
const archiver = require('archiver');
|
|
const archiver = require('archiver');
|
|
|
|
|
+const toArrayIfNot = require('../../lib/util/toArrayIfNot');
|
|
|
|
|
|
|
|
class ExportService {
|
|
class ExportService {
|
|
|
|
|
|
|
|
constructor(crowi) {
|
|
constructor(crowi) {
|
|
|
|
|
+ this.appService = crowi.appService;
|
|
|
this.baseDir = path.join(crowi.tmpDir, 'downloads');
|
|
this.baseDir = path.join(crowi.tmpDir, 'downloads');
|
|
|
- this.extension = 'json';
|
|
|
|
|
this.encoding = 'utf-8';
|
|
this.encoding = 'utf-8';
|
|
|
this.per = 100;
|
|
this.per = 100;
|
|
|
this.zlibLevel = 9; // 0(min) - 9(max)
|
|
this.zlibLevel = 9; // 0(min) - 9(max)
|
|
|
|
|
|
|
|
- this.files = {};
|
|
|
|
|
- // populate this.files
|
|
|
|
|
|
|
+ // path to zip file for exporting multiple collection
|
|
|
|
|
+ this.zipFile = path.join(this.baseDir, 'GROWI.zip');
|
|
|
|
|
+
|
|
|
|
|
+ // { pages: Page, users: User, ... }
|
|
|
|
|
+ this.collectionMap = {};
|
|
|
|
|
+ this.initCollectionMap(crowi.models);
|
|
|
|
|
+
|
|
|
// this.files = {
|
|
// this.files = {
|
|
|
// configs: path.join(this.baseDir, 'configs.json'),
|
|
// configs: path.join(this.baseDir, 'configs.json'),
|
|
|
// pages: path.join(this.baseDir, 'pages.json'),
|
|
// pages: path.join(this.baseDir, 'pages.json'),
|
|
|
// pagetagrelations: path.join(this.baseDir, 'pagetagrelations.json'),
|
|
// pagetagrelations: path.join(this.baseDir, 'pagetagrelations.json'),
|
|
|
// ...
|
|
// ...
|
|
|
// };
|
|
// };
|
|
|
- // TODO: handle 3 globalnotificationsettings collection properly
|
|
|
|
|
- // see Object.values(crowi.models).forEach((m) => { return console.log(m.collection.collectionName) });
|
|
|
|
|
|
|
+ this.files = {};
|
|
|
Object.values(crowi.models).forEach((m) => {
|
|
Object.values(crowi.models).forEach((m) => {
|
|
|
const name = m.collection.collectionName;
|
|
const name = m.collection.collectionName;
|
|
|
- this.files[name] = path.join(this.baseDir, `${name}.${this.extension}`);
|
|
|
|
|
|
|
+ this.files[name] = path.join(this.baseDir, `${name}.json`);
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * initialize collection map
|
|
|
|
|
+ *
|
|
|
|
|
+ * @memberOf ExportService
|
|
|
|
|
+ * @param {object} models from models/index.js
|
|
|
|
|
+ */
|
|
|
|
|
+ initCollectionMap(models) {
|
|
|
|
|
+ for (const model of Object.values(models)) {
|
|
|
|
|
+ this.collectionMap[model.collection.collectionName] = model;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* dump a collection into json
|
|
* dump a collection into json
|
|
|
*
|
|
*
|
|
@@ -49,6 +66,12 @@ class ExportService {
|
|
|
status[path.basename(file, '.zip')] = file;
|
|
status[path.basename(file, '.zip')] = file;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ files.forEach((file) => {
|
|
|
|
|
+ const stats = fs.statSync(path.join(this.baseDir, file));
|
|
|
|
|
+ stats.name = file;
|
|
|
|
|
+ status[path.basename(file, '.zip')] = stats;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
return status;
|
|
return status;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -59,6 +82,7 @@ class ExportService {
|
|
|
* @param {string} file path to json file to be written
|
|
* @param {string} file path to json file to be written
|
|
|
* @param {readStream} readStream read stream
|
|
* @param {readStream} readStream read stream
|
|
|
* @param {number} [total] number of target items (optional)
|
|
* @param {number} [total] number of target items (optional)
|
|
|
|
|
+ * @return {string} path to the exported json file
|
|
|
*/
|
|
*/
|
|
|
async export(file, readStream, total) {
|
|
async export(file, readStream, total) {
|
|
|
let n = 0;
|
|
let n = 0;
|
|
@@ -81,6 +105,8 @@ class ExportService {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
await streamToPromise(readStream);
|
|
await streamToPromise(readStream);
|
|
|
|
|
+
|
|
|
|
|
+ return file;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -90,19 +116,20 @@ class ExportService {
|
|
|
* @param {object} Model instance of mongoose model
|
|
* @param {object} Model instance of mongoose model
|
|
|
* @return {string} path to zip file
|
|
* @return {string} path to zip file
|
|
|
*/
|
|
*/
|
|
|
- async exportCollection(Model) {
|
|
|
|
|
- const modelName = Model.collection.collectionName;
|
|
|
|
|
- const file = this.files[modelName];
|
|
|
|
|
|
|
+ async exportCollectionToJson(Model) {
|
|
|
|
|
+ const { collectionName } = Model.collection;
|
|
|
|
|
+ const targetFile = this.files[collectionName];
|
|
|
const total = await Model.countDocuments();
|
|
const total = await Model.countDocuments();
|
|
|
const readStream = Model.find().cursor();
|
|
const readStream = Model.find().cursor();
|
|
|
|
|
|
|
|
- await this.export(file, readStream, total);
|
|
|
|
|
-
|
|
|
|
|
- const { file: zipFile, size } = await this.zipSingleFile(file);
|
|
|
|
|
|
|
+ const file = await this.export(targetFile, readStream, total);
|
|
|
|
|
|
|
|
- logger.info(`exported ${modelName} collection into ${zipFile} (${size} bytes)`);
|
|
|
|
|
|
|
+ return file;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return zipFile;
|
|
|
|
|
|
|
+ async exportMultipleCollectionsToJsons(models) {
|
|
|
|
|
+ const jsonFiles = await Promise.all(models.map(Model => this.exportCollectionToJson(Model)));
|
|
|
|
|
+ return jsonFiles;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -128,21 +155,18 @@ class ExportService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * zip a file
|
|
|
|
|
|
|
+ * zip files into one zip file
|
|
|
*
|
|
*
|
|
|
* @memberOf ExportService
|
|
* @memberOf ExportService
|
|
|
- * @param {string} from path to input file
|
|
|
|
|
- * @param {string} [to=`${path.join(path.dirname(from), `${path.basename(from, path.extname(from))}.zip`)}`] path to output file
|
|
|
|
|
- * @param {string} [as=path.basename(from)] file name after unzipped
|
|
|
|
|
- * @return {object} file path and file size
|
|
|
|
|
|
|
+ * @param {object|array<object>} configs array of object { from: "path to source file", as: "file name after unzipped" }
|
|
|
|
|
+ * @return {string} path to zip file
|
|
|
* @see https://www.archiverjs.com/#quick-start
|
|
* @see https://www.archiverjs.com/#quick-start
|
|
|
*/
|
|
*/
|
|
|
- async zipSingleFile(from, to = this.replaceExtension(from, 'zip'), as = path.basename(from)) {
|
|
|
|
|
|
|
+ async zipFiles(_configs) {
|
|
|
|
|
+ const configs = toArrayIfNot(_configs);
|
|
|
const archive = archiver('zip', {
|
|
const archive = archiver('zip', {
|
|
|
zlib: { level: this.zlibLevel },
|
|
zlib: { level: this.zlibLevel },
|
|
|
});
|
|
});
|
|
|
- const input = fs.createReadStream(from);
|
|
|
|
|
- const output = fs.createWriteStream(to);
|
|
|
|
|
|
|
|
|
|
// good practice to catch warnings (ie stat failures and other non-blocking errors)
|
|
// good practice to catch warnings (ie stat failures and other non-blocking errors)
|
|
|
archive.on('warning', (err) => {
|
|
archive.on('warning', (err) => {
|
|
@@ -153,8 +177,14 @@ class ExportService {
|
|
|
// good practice to catch this error explicitly
|
|
// good practice to catch this error explicitly
|
|
|
archive.on('error', (err) => { throw err });
|
|
archive.on('error', (err) => { throw err });
|
|
|
|
|
|
|
|
- // append a file from stream
|
|
|
|
|
- archive.append(input, { name: as });
|
|
|
|
|
|
|
+ for (const { from, as } of configs) {
|
|
|
|
|
+ const input = fs.createReadStream(from);
|
|
|
|
|
+
|
|
|
|
|
+ // append a file from stream
|
|
|
|
|
+ archive.append(input, { name: as });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const output = fs.createWriteStream(this.zipFile);
|
|
|
|
|
|
|
|
// pipe archive data to the file
|
|
// pipe archive data to the file
|
|
|
archive.pipe(output);
|
|
archive.pipe(output);
|
|
@@ -165,10 +195,9 @@ class ExportService {
|
|
|
|
|
|
|
|
await streamToPromise(archive);
|
|
await streamToPromise(archive);
|
|
|
|
|
|
|
|
- return {
|
|
|
|
|
- file: to,
|
|
|
|
|
- size: archive.pointer(),
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ logger.debug(`zipped growi data into ${this.zipFile} (${archive.pointer()} bytes)`);
|
|
|
|
|
+
|
|
|
|
|
+ return this.zipFile;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -194,12 +223,29 @@ class ExportService {
|
|
|
const json = this.files[Model.collection.collectionName];
|
|
const json = this.files[Model.collection.collectionName];
|
|
|
const zip = this.replaceExtension(json, 'zip');
|
|
const zip = this.replaceExtension(json, 'zip');
|
|
|
if (!fs.existsSync(zip)) {
|
|
if (!fs.existsSync(zip)) {
|
|
|
- return null;
|
|
|
|
|
|
|
+ throw new Error(`${zip} does not exist`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return zip;
|
|
return zip;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * get a model from collection name
|
|
|
|
|
+ *
|
|
|
|
|
+ * @memberOf ExportService
|
|
|
|
|
+ * @param {object} collectionName collection name
|
|
|
|
|
+ * @return {object} instance of mongoose model
|
|
|
|
|
+ */
|
|
|
|
|
+ getModelFromCollectionName(collectionName) {
|
|
|
|
|
+ const Model = this.collectionMap[collectionName];
|
|
|
|
|
+
|
|
|
|
|
+ if (Model == null) {
|
|
|
|
|
+ throw new Error(`cannot find a model for collection name "${collectionName}"`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Model;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
module.exports = ExportService;
|
|
module.exports = ExportService;
|