import.js 7.8 KB

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