export.js 8.1 KB

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