import.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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 multer = require('multer');
  5. const express = require('express');
  6. const GrowiArchiveImportOption = require('@commons/models/admin/growi-archive-import-option');
  7. const ErrorV3 = require('../../models/vo/error-apiv3');
  8. const router = express.Router();
  9. /**
  10. * @swagger
  11. * tags:
  12. * name: Import
  13. */
  14. /**
  15. * @swagger
  16. *
  17. * components:
  18. * schemas:
  19. * ImportStatus:
  20. * description: ImportStatus
  21. * type: object
  22. * properties:
  23. * zipFileStat:
  24. * type: object
  25. * description: the property object
  26. * progressList:
  27. * type: array
  28. * items:
  29. * type: object
  30. * description: progress data for each exporting collections
  31. * isImporting:
  32. * type: boolean
  33. * description: whether the current importing job exists or not
  34. */
  35. /**
  36. * generate overwrite params with overwrite-params/* modules
  37. * @param {string} collectionName
  38. * @param {object} req Request Object
  39. * @param {GrowiArchiveImportOption} options GrowiArchiveImportOption instance
  40. */
  41. const generateOverwriteParams = (collectionName, req, options) => {
  42. switch (collectionName) {
  43. case 'pages':
  44. return require('./overwrite-params/pages')(req, options);
  45. case 'revisions':
  46. return require('./overwrite-params/revisions')(req, options);
  47. case 'attachmentFiles.chunks':
  48. return require('./overwrite-params/attachmentFiles.chunks')(req, options);
  49. default:
  50. return {};
  51. }
  52. };
  53. module.exports = (crowi) => {
  54. const { growiBridgeService, importService, socketIoService } = crowi;
  55. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  56. const loginRequired = require('../../middlewares/login-required')(crowi);
  57. const adminRequired = require('../../middlewares/admin-required')(crowi);
  58. const csrf = require('../../middlewares/csrf')(crowi);
  59. this.adminEvent = crowi.event('admin');
  60. // setup event
  61. this.adminEvent.on('onProgressForImport', (data) => {
  62. socketIoService.getAdminSocket().emit('admin:onProgressForImport', data);
  63. });
  64. this.adminEvent.on('onTerminateForImport', (data) => {
  65. socketIoService.getAdminSocket().emit('admin:onTerminateForImport', data);
  66. });
  67. this.adminEvent.on('onErrorForImport', (data) => {
  68. socketIoService.getAdminSocket().emit('admin:onErrorForImport', data);
  69. });
  70. const uploads = multer({
  71. storage: multer.diskStorage({
  72. destination: (req, file, cb) => {
  73. cb(null, importService.baseDir);
  74. },
  75. filename(req, file, cb) {
  76. // to prevent hashing the file name. files with same name will be overwritten.
  77. cb(null, file.originalname);
  78. },
  79. }),
  80. fileFilter: (req, file, cb) => {
  81. if (path.extname(file.originalname) === '.zip') {
  82. return cb(null, true);
  83. }
  84. cb(new Error('Only ".zip" is allowed'));
  85. },
  86. });
  87. /**
  88. * @swagger
  89. *
  90. * /import:
  91. * get:
  92. * tags: [Import]
  93. * operationId: getImportSettingsParams
  94. * summary: /import
  95. * description: Get import settings params
  96. * responses:
  97. * 200:
  98. * description: import settings params
  99. * content:
  100. * application/json:
  101. * schema:
  102. * properties:
  103. * importSettingsParams:
  104. * type: object
  105. * description: import settings params
  106. */
  107. router.get('/', accessTokenParser, loginRequired, adminRequired, async(req, res) => {
  108. try {
  109. const importSettingsParams = {
  110. esaTeamName: await crowi.configManager.getConfig('crowi', 'importer:esa:team_name'),
  111. esaAccessToken: await crowi.configManager.getConfig('crowi', 'importer:esa:access_token'),
  112. qiitaTeamName: await crowi.configManager.getConfig('crowi', 'importer:qiita:team_name'),
  113. qiitaAccessToken: await crowi.configManager.getConfig('crowi', 'importer:qiita:access_token'),
  114. };
  115. return res.apiv3({
  116. importSettingsParams,
  117. });
  118. }
  119. catch (err) {
  120. return res.apiv3Err(err, 500);
  121. }
  122. });
  123. /**
  124. * @swagger
  125. *
  126. * /import/status:
  127. * get:
  128. * tags: [Import]
  129. * operationId: getImportStatus
  130. * summary: /import/status
  131. * description: Get properties of stored zip files for import
  132. * responses:
  133. * 200:
  134. * description: the zip file statuses
  135. * content:
  136. * application/json:
  137. * schema:
  138. * properties:
  139. * status:
  140. * $ref: '#/components/schemas/ImportStatus'
  141. */
  142. router.get('/status', accessTokenParser, loginRequired, adminRequired, async(req, res) => {
  143. try {
  144. const status = await importService.getStatus();
  145. return res.apiv3(status);
  146. }
  147. catch (err) {
  148. return res.apiv3Err(err, 500);
  149. }
  150. });
  151. /**
  152. * @swagger
  153. *
  154. * /import:
  155. * post:
  156. * tags: [Import]
  157. * operationId: executeImport
  158. * summary: /import
  159. * description: import a collection from a zipped json
  160. * requestBody:
  161. * required: true
  162. * content:
  163. * application/json:
  164. * schema:
  165. * type: object
  166. * properties:
  167. * fileName:
  168. * description: the file name of zip file
  169. * type: string
  170. * collections:
  171. * description: collection names to import
  172. * type: array
  173. * items:
  174. * type: string
  175. * optionsMap:
  176. * description: |
  177. * the map object of importing option that have collection name as the key
  178. * additionalProperties:
  179. * type: object
  180. * properties:
  181. * mode:
  182. * description: Import mode
  183. * type: string
  184. * enum: [insert, upsert, flushAndInsert]
  185. * responses:
  186. * 200:
  187. * description: Import process has requested
  188. */
  189. router.post('/', accessTokenParser, loginRequired, adminRequired, csrf, async(req, res) => {
  190. // TODO: add express validator
  191. const { fileName, collections, optionsMap } = req.body;
  192. const zipFile = importService.getFile(fileName);
  193. // return response first
  194. res.apiv3();
  195. /*
  196. * unzip, parse
  197. */
  198. let meta = null;
  199. let fileStatsToImport = null;
  200. try {
  201. // unzip
  202. await importService.unzip(zipFile);
  203. // eslint-disable-next-line no-unused-vars
  204. const { meta: parsedMeta, fileStats, innerFileStats } = await growiBridgeService.parseZipFile(zipFile);
  205. meta = parsedMeta;
  206. // filter innerFileStats
  207. fileStatsToImport = innerFileStats.filter(({ fileName, collectionName, size }) => {
  208. return collections.includes(collectionName);
  209. });
  210. }
  211. catch (err) {
  212. logger.error(err);
  213. this.adminEvent.emit('onErrorForImport', { message: err.message });
  214. return;
  215. }
  216. /*
  217. * validate with meta.json
  218. */
  219. try {
  220. importService.validate(meta);
  221. }
  222. catch (err) {
  223. logger.error(err);
  224. this.adminEvent.emit('onErrorForImport', { message: err.message });
  225. return res.apiv3Err(err);
  226. // return;
  227. }
  228. // generate maps of ImportSettings to import
  229. const importSettingsMap = {};
  230. fileStatsToImport.forEach(({ fileName, collectionName }) => {
  231. // instanciate GrowiArchiveImportOption
  232. const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
  233. // generate options
  234. const importSettings = importService.generateImportSettings(options.mode);
  235. importSettings.jsonFileName = fileName;
  236. // generate overwrite params
  237. importSettings.overwriteParams = generateOverwriteParams(collectionName, req, options);
  238. importSettingsMap[collectionName] = importSettings;
  239. });
  240. /*
  241. * import
  242. */
  243. try {
  244. importService.import(collections, importSettingsMap);
  245. }
  246. catch (err) {
  247. logger.error(err);
  248. this.adminEvent.emit('onErrorForImport', { message: err.message });
  249. }
  250. });
  251. /**
  252. * @swagger
  253. *
  254. * /import/upload:
  255. * post:
  256. * tags: [Import]
  257. * operationId: uploadImport
  258. * summary: /import/upload
  259. * description: upload a zip file
  260. * responses:
  261. * 200:
  262. * description: the file is uploaded
  263. * content:
  264. * application/json:
  265. * schema:
  266. * properties:
  267. * meta:
  268. * type: object
  269. * description: the meta data of the uploaded file
  270. * fileName:
  271. * type: string
  272. * description: the base name of the uploaded file
  273. * fileStats:
  274. * type: array
  275. * items:
  276. * type: object
  277. * description: the property of each extracted file
  278. */
  279. router.post('/upload', uploads.single('file'), accessTokenParser, loginRequired, adminRequired, csrf, async(req, res) => {
  280. const { file } = req;
  281. const zipFile = importService.getFile(file.filename);
  282. try {
  283. const data = await growiBridgeService.parseZipFile(zipFile);
  284. }
  285. catch (err) {
  286. console.log('aaa');
  287. const varidationErr = 'versions-are-not-met';
  288. // TODO: use ApiV3Error
  289. logger.error(err);
  290. return res.status(500).send({ status: 'ERROR' });
  291. }
  292. try {
  293. console.log('bbb');
  294. // validate with meta.json
  295. importService.validate(data.meta);
  296. } catch {
  297. console.log('ccc');
  298. const msg = 'the version of this growi and the growi that exported the data are not met'
  299. const varidationErr = 'versions-are-not-met';
  300. return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
  301. }
  302. });
  303. // try {
  304. // importService.validate(meta);
  305. // }
  306. // catch (err) {
  307. // const varidationErr = 'versions-are-not-met';
  308. // logger.error(err);
  309. // this.adminEvent.emit('onErrorForImport', { message: err.message });
  310. // return res.apiv3Err(varidationErr, 500);
  311. // }
  312. /**
  313. * @swagger
  314. *
  315. * /import/all:
  316. * delete:
  317. * tags: [Import]
  318. * operationId: deleteImportAll
  319. * summary: /import/all
  320. * description: Delete all zip files
  321. * responses:
  322. * 200:
  323. * description: all files are deleted
  324. */
  325. router.delete('/all', accessTokenParser, loginRequired, adminRequired, csrf, async(req, res) => {
  326. try {
  327. importService.deleteAllZipFiles();
  328. return res.apiv3();
  329. }
  330. catch (err) {
  331. logger.error(err);
  332. return res.apiv3Err(err, 500);
  333. }
  334. });
  335. return router;
  336. };