import.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. const loggerFactory = require('@alias/logger');
  2. const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-line no-unused-vars
  3. const path = require('path');
  4. const fs = require('fs');
  5. const multer = require('multer');
  6. const { ObjectId } = require('mongoose').Types;
  7. const express = require('express');
  8. const router = express.Router();
  9. /**
  10. * @swagger
  11. * tags:
  12. * name: Import
  13. */
  14. module.exports = (crowi) => {
  15. const { growiBridgeService, importService } = crowi;
  16. const uploads = multer({
  17. storage: multer.diskStorage({
  18. destination: (req, file, cb) => {
  19. cb(null, importService.baseDir);
  20. },
  21. filename(req, file, cb) {
  22. // to prevent hashing the file name. files with same name will be overwritten.
  23. cb(null, file.originalname);
  24. },
  25. }),
  26. fileFilter: (req, file, cb) => {
  27. if (path.extname(file.originalname) === '.zip') {
  28. return cb(null, true);
  29. }
  30. cb(new Error('Only ".zip" is allowed'));
  31. },
  32. });
  33. /**
  34. * defined overwrite params for each collection
  35. * all imported documents are overwriten by this value
  36. * each value can be any value or a function (_value, { _document, key, schema }) { return newValue }
  37. *
  38. * @param {object} Model instance of mongoose model
  39. * @param {object} req request object
  40. * @return {object} document to be persisted
  41. */
  42. const overwriteParamsFn = async(Model, schema, req) => {
  43. const { collectionName } = Model.collection;
  44. /* eslint-disable no-case-declarations */
  45. switch (Model.collection.collectionName) {
  46. case 'pages':
  47. // TODO: use schema and req to generate overwriteParams
  48. // e.g. { creator: schema.creator === 'me' ? ObjectId(req.user._id) : importService.keepOriginal }
  49. return {
  50. status: 'published', // FIXME when importing users and user groups
  51. grant: 1, // FIXME when importing users and user groups
  52. grantedUsers: [], // FIXME when importing users and user groups
  53. grantedGroup: null, // FIXME when importing users and user groups
  54. creator: ObjectId(req.user._id), // FIXME when importing users
  55. lastUpdateUser: ObjectId(req.user._id), // FIXME when importing users
  56. liker: [], // FIXME when importing users
  57. seenUsers: [], // FIXME when importing users
  58. commentCount: 0, // FIXME when importing comments
  59. extended: {}, // FIXME when ?
  60. pageIdOnHackmd: undefined, // FIXME when importing hackmd?
  61. revisionHackmdSynced: undefined, // FIXME when importing hackmd?
  62. hasDraftOnHackmd: undefined, // FIXME when importing hackmd?
  63. };
  64. // case 'revisoins':
  65. // return {};
  66. // case 'users':
  67. // return {};
  68. // ... add more cases
  69. default:
  70. throw new Error(`cannot find a model for collection name "${collectionName}"`);
  71. }
  72. /* eslint-enable no-case-declarations */
  73. };
  74. /**
  75. * @swagger
  76. *
  77. * /import:
  78. * post:
  79. * tags: [Import]
  80. * description: import a collection from a zipped json
  81. * responses:
  82. * 200:
  83. * description: the data is successfully imported
  84. * content:
  85. * application/json:
  86. * schema:
  87. * properties:
  88. * results:
  89. * type: array
  90. * items:
  91. * type: object
  92. * description: collectionName, insertedIds, failedIds
  93. */
  94. router.post('/', async(req, res) => {
  95. // TODO: add express validator
  96. const { fileName, collections, schema } = req.body;
  97. const zipFile = importService.getFile(fileName);
  98. // unzip
  99. await importService.unzip(zipFile);
  100. // eslint-disable-next-line no-unused-vars
  101. const { meta, fileStats } = await growiBridgeService.parseZipFile(zipFile);
  102. // delete zip file after unzipping and parsing it
  103. fs.unlinkSync(zipFile);
  104. // filter fileStats
  105. const filteredFileStats = fileStats.filter(({ fileName, collectionName, size }) => { return collections.includes(collectionName) });
  106. try {
  107. // validate with meta.json
  108. importService.validate(meta);
  109. const results = await Promise.all(filteredFileStats.map(async({ fileName, collectionName, size }) => {
  110. const Model = growiBridgeService.getModelFromCollectionName(collectionName);
  111. const jsonFile = importService.getFile(fileName);
  112. let overwriteParams;
  113. if (overwriteParamsFn[collectionName] != null) {
  114. // await in case overwriteParamsFn[collection] is a Promise
  115. overwriteParams = await overwriteParamsFn(Model, schema[collectionName], req);
  116. }
  117. const { insertedIds, failedIds } = await importService.import(Model, jsonFile, overwriteParams);
  118. return {
  119. collectionName,
  120. insertedIds,
  121. failedIds,
  122. };
  123. }));
  124. // TODO: use res.apiv3
  125. return res.send({ ok: true, results });
  126. }
  127. catch (err) {
  128. // TODO: use ApiV3Error
  129. logger.error(err);
  130. return res.status(500).send({ status: 'ERROR' });
  131. }
  132. });
  133. /**
  134. * @swagger
  135. *
  136. * /import/upload:
  137. * post:
  138. * tags: [Import]
  139. * description: upload a zip file
  140. * responses:
  141. * 200:
  142. * description: the file is uploaded
  143. * content:
  144. * application/json:
  145. * schema:
  146. * properties:
  147. * meta:
  148. * type: object
  149. * description: the meta data of the uploaded file
  150. * fileName:
  151. * type: string
  152. * description: the base name of the uploaded file
  153. * fileStats:
  154. * type: array
  155. * items:
  156. * type: object
  157. * description: the property of each extracted file
  158. */
  159. router.post('/upload', uploads.single('file'), async(req, res) => {
  160. const { file } = req;
  161. const zipFile = importService.getFile(file.filename);
  162. try {
  163. const data = await growiBridgeService.parseZipFile(zipFile);
  164. // validate with meta.json
  165. importService.validate(data.meta);
  166. // TODO: use res.apiv3
  167. return res.send({
  168. ok: true,
  169. data,
  170. });
  171. }
  172. catch (err) {
  173. // TODO: use ApiV3Error
  174. logger.error(err);
  175. return res.status(500).send({ status: 'ERROR' });
  176. }
  177. });
  178. /**
  179. * @swagger
  180. *
  181. * /import/{fileName}:
  182. * post:
  183. * tags: [Import]
  184. * description: delete a zip file
  185. * parameters:
  186. * - name: fileName
  187. * in: path
  188. * description: the file name of zip file
  189. * required: true
  190. * schema:
  191. * type: string
  192. * responses:
  193. * 200:
  194. * description: the file is deleted
  195. * content:
  196. * application/json:
  197. * schema:
  198. * type: object
  199. */
  200. router.delete('/:fileName', async(req, res) => {
  201. const { fileName } = req.params;
  202. try {
  203. const zipFile = importService.getFile(fileName);
  204. fs.unlinkSync(zipFile);
  205. // TODO: use res.apiv3
  206. return res.send({
  207. ok: true,
  208. });
  209. }
  210. catch (err) {
  211. // TODO: use ApiV3Error
  212. logger.error(err);
  213. return res.status(500).send({ status: 'ERROR' });
  214. }
  215. });
  216. return router;
  217. };