|
|
@@ -7,28 +7,44 @@ const archiver = require('archiver');
|
|
|
class ExportService {
|
|
|
|
|
|
constructor(crowi) {
|
|
|
+ this.appService = crowi.appService;
|
|
|
this.baseDir = path.join(crowi.tmpDir, 'downloads');
|
|
|
- this.extension = 'json';
|
|
|
this.encoding = 'utf-8';
|
|
|
this.per = 100;
|
|
|
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 = {
|
|
|
// configs: path.join(this.baseDir, 'configs.json'),
|
|
|
// pages: path.join(this.baseDir, 'pages.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) => {
|
|
|
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
|
|
|
*
|
|
|
@@ -49,6 +65,12 @@ class ExportService {
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
@@ -59,6 +81,7 @@ class ExportService {
|
|
|
* @param {string} file path to json file to be written
|
|
|
* @param {readStream} readStream read stream
|
|
|
* @param {number} [total] number of target items (optional)
|
|
|
+ * @return {string} path to the exported json file
|
|
|
*/
|
|
|
async export(file, readStream, total) {
|
|
|
let n = 0;
|
|
|
@@ -81,6 +104,8 @@ class ExportService {
|
|
|
});
|
|
|
|
|
|
await streamToPromise(readStream);
|
|
|
+
|
|
|
+ return file;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -90,19 +115,20 @@ class ExportService {
|
|
|
* @param {object} Model instance of mongoose model
|
|
|
* @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 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;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -134,7 +160,7 @@ class 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
|
|
|
+ * @return {string} path to zip file
|
|
|
* @see https://www.archiverjs.com/#quick-start
|
|
|
*/
|
|
|
async zipSingleFile(from, to = this.replaceExtension(from, 'zip'), as = path.basename(from)) {
|
|
|
@@ -165,10 +191,52 @@ class ExportService {
|
|
|
|
|
|
await streamToPromise(archive);
|
|
|
|
|
|
- return {
|
|
|
- file: to,
|
|
|
- size: archive.pointer(),
|
|
|
- };
|
|
|
+ logger.debug(`zipped ${from} into ${to} (${archive.pointer()} bytes)`);
|
|
|
+
|
|
|
+ return to;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * zip a file
|
|
|
+ *
|
|
|
+ * @memberOf ExportService
|
|
|
+ * @param {array} configs array of object { from: "path to source file", as: "file name appears after unzipped" }
|
|
|
+ * @return {string} path to zip file
|
|
|
+ */
|
|
|
+ async zipMultipleFiles(configs) {
|
|
|
+ const archive = archiver('zip', {
|
|
|
+ zlib: { level: this.zlibLevel },
|
|
|
+ });
|
|
|
+ const output = fs.createWriteStream(this.zipFile);
|
|
|
+
|
|
|
+ // good practice to catch warnings (ie stat failures and other non-blocking errors)
|
|
|
+ archive.on('warning', (err) => {
|
|
|
+ if (err.code === 'ENOENT') logger.error(err);
|
|
|
+ else throw err;
|
|
|
+ });
|
|
|
+
|
|
|
+ // good practice to catch this error explicitly
|
|
|
+ archive.on('error', (err) => { throw err });
|
|
|
+
|
|
|
+ for (const { from, as } of configs) {
|
|
|
+ const input = fs.createReadStream(from);
|
|
|
+
|
|
|
+ // append a file from stream
|
|
|
+ archive.append(input, { name: as });
|
|
|
+ }
|
|
|
+
|
|
|
+ // pipe archive data to the file
|
|
|
+ archive.pipe(output);
|
|
|
+
|
|
|
+ // finalize the archive (ie we are done appending files but streams have to finish yet)
|
|
|
+ // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
|
|
|
+ archive.finalize();
|
|
|
+
|
|
|
+ await streamToPromise(archive);
|
|
|
+
|
|
|
+ logger.debug(`zipped growi data into ${this.zipFile} (${archive.pointer()} bytes)`);
|
|
|
+
|
|
|
+ return this.zipFile;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -194,12 +262,29 @@ class ExportService {
|
|
|
const json = this.files[Model.collection.collectionName];
|
|
|
const zip = this.replaceExtension(json, 'zip');
|
|
|
if (!fs.existsSync(zip)) {
|
|
|
- return null;
|
|
|
+ throw new Error(`${zip} does not exist`);
|
|
|
}
|
|
|
|
|
|
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;
|