export.js 8.0 KB

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