export.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import { SupportedAction } from '~/interfaces/activity';
  2. import { SCOPE } from '~/interfaces/scope';
  3. import { accessTokenParser } from '~/server/middlewares/access-token-parser';
  4. import { exportService } from '~/server/service/export';
  5. import loggerFactory from '~/utils/logger';
  6. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  7. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  8. const logger = loggerFactory('growi:routes:apiv3:export');
  9. const fs = require('fs');
  10. const express = require('express');
  11. const { param } = require('express-validator');
  12. const router = express.Router();
  13. /**
  14. * @swagger
  15. *
  16. * components:
  17. * schemas:
  18. * ExportStatus:
  19. * type: object
  20. * properties:
  21. * zipFileStats:
  22. * type: array
  23. * items:
  24. * $ref: '#/components/schemas/ExportZipFileStat'
  25. * isExporting:
  26. * type: boolean
  27. * progressList:
  28. * type: array
  29. * nullable: true
  30. * items:
  31. * type: string
  32. * ExportZipFileStat:
  33. * type: object
  34. * properties:
  35. * meta:
  36. * $ref: '#/components/schemas/ExportMeta'
  37. * fileName:
  38. * type: string
  39. * zipFilePath:
  40. * type: string
  41. * fileStat:
  42. * $ref: '#/components/schemas/ExportFileStat'
  43. * innerFileStats:
  44. * type: array
  45. * items:
  46. * $ref: '#/components/schemas/ExportInnerFileStat'
  47. * ExportMeta:
  48. * type: object
  49. * properties:
  50. * version:
  51. * type: string
  52. * url:
  53. * type: string
  54. * passwordSeed:
  55. * type: string
  56. * exportedAt:
  57. * type: string
  58. * format: date-time
  59. * envVars:
  60. * type: object
  61. * additionalProperties:
  62. * type: string
  63. * ExportFileStat:
  64. * type: object
  65. * properties:
  66. * dev:
  67. * type: integer
  68. * mode:
  69. * type: integer
  70. * nlink:
  71. * type: integer
  72. * uid:
  73. * type: integer
  74. * gid:
  75. * type: integer
  76. * rdev:
  77. * type: integer
  78. * blksize:
  79. * type: integer
  80. * ino:
  81. * type: integer
  82. * size:
  83. * type: integer
  84. * blocks:
  85. * type: integer
  86. * atime:
  87. * type: string
  88. * format: date-time
  89. * mtime:
  90. * type: string
  91. * format: date-time
  92. * ctime:
  93. * type: string
  94. * format: date-time
  95. * birthtime:
  96. * type: string
  97. * format: date-time
  98. * ExportInnerFileStat:
  99. * type: object
  100. * properties:
  101. * fileName:
  102. * type: string
  103. * collectionName:
  104. * type: string
  105. * meta:
  106. * type: object
  107. * properties:
  108. * progressList:
  109. * type: array
  110. * items:
  111. * type: object
  112. * description: progress data for each exporting collections
  113. * isExporting:
  114. * type: boolean
  115. * description: whether the current exporting job exists or not
  116. */
  117. /** @param {import('~/server/crowi').default} crowi Crowi instance */
  118. module.exports = (crowi) => {
  119. const loginRequired = require('../../middlewares/login-required')(crowi);
  120. const adminRequired = require('../../middlewares/admin-required')(crowi);
  121. const addActivity = generateAddActivityMiddleware(crowi);
  122. const { socketIoService } = crowi;
  123. const activityEvent = crowi.event('activity');
  124. const adminEvent = crowi.event('admin');
  125. // setup event
  126. adminEvent.on('onProgressForExport', (data) => {
  127. socketIoService.getAdminSocket().emit('admin:onProgressForExport', data);
  128. });
  129. adminEvent.on('onStartZippingForExport', (data) => {
  130. socketIoService.getAdminSocket().emit('admin:onStartZippingForExport', data);
  131. });
  132. adminEvent.on('onTerminateForExport', (data) => {
  133. socketIoService.getAdminSocket().emit('admin:onTerminateForExport', data);
  134. });
  135. const validator = {
  136. deleteFile: [
  137. // https://regex101.com/r/mD4eZs/6
  138. // prevent from unexpecting attack doing delete file (path traversal attack)
  139. param('fileName').not().matches(/(\.\.\/|\.\.\\)/),
  140. ],
  141. };
  142. /**
  143. * @swagger
  144. *
  145. * /export/status:
  146. * get:
  147. * tags: [Export]
  148. * operationId: getExportStatus
  149. * summary: /export/status
  150. * description: get properties of stored zip files for export
  151. * responses:
  152. * 200:
  153. * description: the zip file statuses
  154. * content:
  155. * application/json:
  156. * schema:
  157. * properties:
  158. * ok:
  159. * type: boolean
  160. * description: whether the request is succeeded or not
  161. * status:
  162. * $ref: '#/components/schemas/ExportStatus'
  163. */
  164. router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
  165. const status = await exportService.getStatus();
  166. // TODO: use res.apiv3
  167. return res.json({
  168. ok: true,
  169. status,
  170. });
  171. });
  172. /**
  173. * @swagger
  174. *
  175. * /export:
  176. * post:
  177. * tags: [Export]
  178. * operationId: createExport
  179. * summary: /export
  180. * description: generate zipped jsons for collections
  181. * requestBody:
  182. * content:
  183. * application/json:
  184. * schema:
  185. * properties:
  186. * collections:
  187. * type: array
  188. * items:
  189. * type: string
  190. * description: the collections to export
  191. * example: ["pages", "tags"]
  192. * responses:
  193. * 200:
  194. * description: a zip file is generated
  195. * content:
  196. * application/json:
  197. * schema:
  198. * properties:
  199. * ok:
  200. * type: boolean
  201. * description: whether the request is succeeded
  202. */
  203. router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired, addActivity, async(req, res) => {
  204. // TODO: add express validator
  205. try {
  206. const { collections } = req.body;
  207. exportService.export(collections);
  208. const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_CREATE };
  209. activityEvent.emit('update', res.locals.activity._id, parameters);
  210. // TODO: use res.apiv3
  211. return res.status(200).json({
  212. ok: true,
  213. });
  214. }
  215. catch (err) {
  216. // TODO: use ApiV3Error
  217. logger.error(err);
  218. return res.status(500).send({ status: 'ERROR' });
  219. }
  220. });
  221. /**
  222. * @swagger
  223. *
  224. * /export/{fileName}:
  225. * delete:
  226. * tags: [Export]
  227. * operationId: deleteExport
  228. * summary: /export/{fileName}
  229. * description: delete the file
  230. * parameters:
  231. * - name: fileName
  232. * in: path
  233. * description: the file name of zip file
  234. * required: true
  235. * schema:
  236. * type: string
  237. * responses:
  238. * 200:
  239. * description: the file is deleted
  240. * content:
  241. * application/json:
  242. * schema:
  243. * type: object
  244. * properties:
  245. * ok:
  246. * type: boolean
  247. * description: whether the request is succeeded
  248. */
  249. router.delete('/:fileName', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired,
  250. validator.deleteFile, apiV3FormValidator, addActivity,
  251. async(req, res) => {
  252. // TODO: add express validator
  253. const { fileName } = req.params;
  254. try {
  255. const zipFile = exportService.getFile(fileName);
  256. fs.unlinkSync(zipFile);
  257. const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_DELETE };
  258. activityEvent.emit('update', res.locals.activity._id, parameters);
  259. // TODO: use res.apiv3
  260. return res.status(200).send({ ok: true });
  261. }
  262. catch (err) {
  263. // TODO: use ApiV3Error
  264. logger.error(err);
  265. return res.status(500).send({ ok: false });
  266. }
  267. });
  268. return router;
  269. };