attachment.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import { ErrorV3 } from '@growi/core/dist/models';
  2. import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
  3. import express from 'express';
  4. import multer from 'multer';
  5. import autoReap from 'multer-autoreap';
  6. import { SupportedAction } from '~/interfaces/activity';
  7. import { AttachmentType } from '~/server/interfaces/attachment';
  8. import { Attachment } from '~/server/models/attachment';
  9. import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
  10. import loggerFactory from '~/utils/logger';
  11. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  12. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  13. import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
  14. import { excludeReadOnlyUser } from '../../middlewares/exclude-read-only-user';
  15. const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable-line no-unused-vars
  16. const router = express.Router();
  17. const {
  18. query, param, body,
  19. } = require('express-validator');
  20. /**
  21. * @swagger
  22. * tags:
  23. * name: Attachment
  24. */
  25. /**
  26. * @swagger
  27. *
  28. * components:
  29. * schemas:
  30. * Attachment:
  31. * description: Attachment
  32. * type: object
  33. * properties:
  34. * _id:
  35. * type: string
  36. * description: attachment ID
  37. * example: 5e0734e072560e001761fa67
  38. * __v:
  39. * type: number
  40. * description: attachment version
  41. * example: 0
  42. * fileFormat:
  43. * type: string
  44. * description: file format in MIME
  45. * example: text/plain
  46. * fileName:
  47. * type: string
  48. * description: file name
  49. * example: 601b7c59d43a042c0117e08dd37aad0aimage.txt
  50. * originalName:
  51. * type: string
  52. * description: original file name
  53. * example: file.txt
  54. * creator:
  55. * $ref: '#/components/schemas/User'
  56. * page:
  57. * type: string
  58. * description: page ID attached at
  59. * example: 5e07345972560e001761fa63
  60. * createdAt:
  61. * type: string
  62. * description: date created at
  63. * example: 2010-01-01T00:00:00.000Z
  64. * fileSize:
  65. * type: number
  66. * description: file size
  67. * example: 3494332
  68. * url:
  69. * type: string
  70. * description: attachment URL
  71. * example: http://localhost/files/5e0734e072560e001761fa67
  72. * filePathProxied:
  73. * type: string
  74. * description: file path proxied
  75. * example: "/attachment/5e0734e072560e001761fa67"
  76. * downloadPathProxied:
  77. * type: string
  78. * description: download path proxied
  79. * example: "/download/5e0734e072560e001761fa67"
  80. */
  81. module.exports = (crowi) => {
  82. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  83. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  84. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  85. const Page = crowi.model('Page');
  86. const User = crowi.model('User');
  87. const { attachmentService } = crowi;
  88. const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
  89. const addActivity = generateAddActivityMiddleware(crowi);
  90. const activityEvent = crowi.event('activity');
  91. const validator = {
  92. retrieveAttachment: [
  93. param('id').isMongoId().withMessage('attachment id is required'),
  94. ],
  95. retrieveAttachments: [
  96. query('pageId').isMongoId().withMessage('pageId is required'),
  97. query('pageNumber').optional().isInt().withMessage('pageNumber must be a number'),
  98. query('limit').optional().isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
  99. ],
  100. retrieveFileLimit: [
  101. query('fileSize').isNumeric().exists({ checkNull: true }).withMessage('fileSize is required'),
  102. ],
  103. retrieveAddAttachment: [
  104. body('page_id').isString().exists({ checkNull: true }).withMessage('page_id is required'),
  105. ],
  106. };
  107. /**
  108. * @swagger
  109. *
  110. * /attachment/list:
  111. * get:
  112. * tags: [Attachment]
  113. * description: Get attachment list
  114. * responses:
  115. * 200:
  116. * description: Return attachment list
  117. * parameters:
  118. * - name: page_id
  119. * in: query
  120. * required: true
  121. * description: page id
  122. * schema:
  123. * type: string
  124. */
  125. router.get('/list', accessTokenParser, loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
  126. const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
  127. const pageNumber = req.query.pageNumber || 1;
  128. const offset = (pageNumber - 1) * limit;
  129. try {
  130. const pageId = req.query.pageId;
  131. // check whether accessible
  132. const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
  133. if (!isAccessible) {
  134. const msg = 'Current user is not accessible to this page.';
  135. return res.apiv3Err(new ErrorV3(msg, 'attachment-list-failed'), 403);
  136. }
  137. // directly get paging-size from db. not to delivery from client side.
  138. const paginateResult = await Attachment.paginate(
  139. { page: pageId },
  140. {
  141. limit,
  142. offset,
  143. populate: 'creator',
  144. },
  145. );
  146. paginateResult.docs.forEach((doc) => {
  147. if (doc.creator != null && doc.creator instanceof User) {
  148. doc.creator = serializeUserSecurely(doc.creator);
  149. }
  150. });
  151. return res.apiv3({ paginateResult });
  152. }
  153. catch (err) {
  154. logger.error('Attachment not found', err);
  155. return res.apiv3Err(err, 500);
  156. }
  157. });
  158. /**
  159. * @swagger
  160. *
  161. * /attachment/limit:
  162. * get:
  163. * tags: [Attachment]
  164. * operationId: getAttachmentLimit
  165. * summary: /attachment/limit
  166. * description: Get available capacity of uploaded file with GridFS
  167. * parameters:
  168. * - in: query
  169. * name: fileSize
  170. * schema:
  171. * type: number
  172. * description: file size
  173. * example: 23175
  174. * required: true
  175. * responses:
  176. * 200:
  177. * description: Succeeded to get available capacity of uploaded file with GridFS.
  178. * content:
  179. * application/json:
  180. * schema:
  181. * properties:
  182. * isUploadable:
  183. * type: boolean
  184. * description: uploadable
  185. * example: true
  186. * 403:
  187. * $ref: '#/components/responses/403'
  188. * 500:
  189. * $ref: '#/components/responses/500'
  190. */
  191. /**
  192. * @api {get} /attachment/limit get available capacity of uploaded file with GridFS
  193. * @apiName AddAttachment
  194. * @apiGroup Attachment
  195. */
  196. router.get('/limit', accessTokenParser, loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator, async(req, res) => {
  197. const { fileUploadService } = crowi;
  198. const fileSize = Number(req.query.fileSize);
  199. try {
  200. return res.apiv3(await fileUploadService.checkLimit(fileSize));
  201. }
  202. catch (err) {
  203. logger.error('File limit retrieval failed', err);
  204. return res.apiv3Err(err, 500);
  205. }
  206. });
  207. /**
  208. * @swagger
  209. *
  210. * /attachment:
  211. * post:
  212. * tags: [Attachment, CrowiCompatibles]
  213. * operationId: addAttachment
  214. * summary: /attachment
  215. * description: Add attachment to the page
  216. * requestBody:
  217. * content:
  218. * "multipart/form-data":
  219. * schema:
  220. * properties:
  221. * page_id:
  222. * nullable: true
  223. * type: string
  224. * path:
  225. * nullable: true
  226. * type: string
  227. * file:
  228. * type: string
  229. * format: binary
  230. * description: attachment data
  231. * encoding:
  232. * path:
  233. * contentType: application/x-www-form-urlencoded
  234. * "*\/*":
  235. * schema:
  236. * properties:
  237. * page_id:
  238. * nullable: true
  239. * type: string
  240. * path:
  241. * nullable: true
  242. * type: string
  243. * file:
  244. * type: string
  245. * format: binary
  246. * description: attachment data
  247. * encoding:
  248. * path:
  249. * contentType: application/x-www-form-urlencoded
  250. * responses:
  251. * 200:
  252. * description: Succeeded to add attachment.
  253. * content:
  254. * application/json:
  255. * schema:
  256. * properties:
  257. * page:
  258. * $ref: '#/components/schemas/Page'
  259. * attachment:
  260. * $ref: '#/components/schemas/Attachment'
  261. * url:
  262. * $ref: '#/components/schemas/Attachment/properties/url'
  263. * pageCreated:
  264. * type: boolean
  265. * description: whether the page was created
  266. * example: false
  267. * 403:
  268. * $ref: '#/components/responses/403'
  269. * 500:
  270. * $ref: '#/components/responses/500'
  271. */
  272. /**
  273. * @api {post} /attachment Add attachment to the page
  274. * @apiName AddAttachment
  275. * @apiGroup Attachment
  276. *
  277. * @apiParam {String} page_id
  278. * @apiParam {String} path
  279. * @apiParam {File} file
  280. */
  281. router.post('/', uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser,
  282. validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
  283. async(req, res) => {
  284. const pageId = req.body.page_id;
  285. // check params
  286. const file = req.file || null;
  287. if (file == null) {
  288. return res.apiv3Err('File error.');
  289. }
  290. try {
  291. const page = await Page.findById(pageId);
  292. // check the user is accessible
  293. const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
  294. if (!isAccessible) {
  295. return res.apiv3Err(`Forbidden to access to the page '${page.id}'`);
  296. }
  297. const attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
  298. const result = {
  299. page: serializePageSecurely(page),
  300. revision: serializeRevisionSecurely(page.revision),
  301. attachment: attachment.toObject({ virtuals: true }),
  302. };
  303. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
  304. res.apiv3(result);
  305. }
  306. catch (err) {
  307. logger.error(err);
  308. return res.apiv3Err(err.message);
  309. }
  310. });
  311. /**
  312. * @swagger
  313. *
  314. * /attachment/{id}:
  315. * get:
  316. * tags: [Attachment]
  317. * description: Get attachment
  318. * responses:
  319. * 200:
  320. * description: Return attachment
  321. * parameters:
  322. * - name: id
  323. * in: path
  324. * required: true
  325. * description: attachment id
  326. * schema:
  327. * type: string
  328. */
  329. router.get('/:id', accessTokenParser, certifySharedPageAttachmentMiddleware, loginRequired, validator.retrieveAttachment, apiV3FormValidator,
  330. async(req, res) => {
  331. try {
  332. const attachmentId = req.params.id;
  333. const attachment = await Attachment.findById(attachmentId).populate('creator').exec();
  334. if (attachment == null) {
  335. const message = 'Attachment not found';
  336. return res.apiv3Err(message, 404);
  337. }
  338. if (attachment.creator != null && attachment.creator instanceof User) {
  339. attachment.creator = serializeUserSecurely(attachment.creator);
  340. }
  341. return res.apiv3({ attachment });
  342. }
  343. catch (err) {
  344. logger.error('Attachment retrieval failed', err);
  345. return res.apiv3Err(err, 500);
  346. }
  347. });
  348. return router;
  349. };