export.js 8.3 KB

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