2
0

import.js 12 KB

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