attachment.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /* eslint-disable no-use-before-define */
  2. const logger = require('@alias/logger')('growi:routes:attachment');
  3. const fs = require('fs');
  4. const ApiResponse = require('../util/apiResponse');
  5. /**
  6. * @swagger
  7. * tags:
  8. * name: Attachments
  9. */
  10. /**
  11. * @swagger
  12. *
  13. * components:
  14. * schemas:
  15. * Attachment:
  16. * description: Attachment
  17. * type: object
  18. * properties:
  19. * _id:
  20. * type: string
  21. * description: attachment ID
  22. * example: 5e0734e072560e001761fa67
  23. * __v:
  24. * type: number
  25. * description: attachment version
  26. * example: 0
  27. * fileFormat:
  28. * type: string
  29. * description: file format in MIME
  30. * example: text/plain
  31. * fileName:
  32. * type: string
  33. * description: file name
  34. * example: 601b7c59d43a042c0117e08dd37aad0aimage.txt
  35. * originalName:
  36. * type: string
  37. * description: original file name
  38. * example: file.txt
  39. * filePath:
  40. * type: string
  41. * description: file path
  42. * example: attachment/5e07345972560e001761fa63/6b0b3facf3628699263d760e18efd446.txt
  43. * creator:
  44. * $ref: '#/components/schemas/User'
  45. * page:
  46. * type: string
  47. * description: page ID attached at
  48. * example: 5e07345972560e001761fa63
  49. * createdAt:
  50. * type: string
  51. * description: date created at
  52. * example: 2010-01-01T00:00:00.000Z
  53. * fileSize:
  54. * type: number
  55. * description: file size
  56. * example: 3494332
  57. * url:
  58. * type: string
  59. * description: attachment URL
  60. * example: http://localhost/files/5e0734e072560e001761fa67
  61. */
  62. module.exports = function(crowi, app) {
  63. const Attachment = crowi.model('Attachment');
  64. const User = crowi.model('User');
  65. const Page = crowi.model('Page');
  66. const fileUploader = require('../service/file-uploader')(crowi, app);
  67. /**
  68. * Check the user is accessible to the related page
  69. *
  70. * @param {User} user
  71. * @param {Attachment} attachment
  72. */
  73. async function isAccessibleByViewer(user, attachment) {
  74. if (attachment.page != null) {
  75. // eslint-disable-next-line no-return-await
  76. return await Page.isAccessiblePageByViewer(attachment.page, user);
  77. }
  78. return true;
  79. }
  80. /**
  81. * Check the user is accessible to the related page
  82. *
  83. * @param {User} user
  84. * @param {Attachment} attachment
  85. */
  86. async function isDeletableByUser(user, attachment) {
  87. const ownerId = attachment.creator._id || attachment.creator;
  88. if (attachment.page == null) { // when profile image
  89. return user.id === ownerId.toString();
  90. }
  91. // eslint-disable-next-line no-return-await
  92. return await Page.isAccessiblePageByViewer(attachment.page, user);
  93. }
  94. /**
  95. * Common method to response
  96. *
  97. * @param {Request} req
  98. * @param {Response} res
  99. * @param {User} user
  100. * @param {Attachment} attachment
  101. * @param {boolean} forceDownload
  102. */
  103. async function responseForAttachment(req, res, attachment, forceDownload) {
  104. if (attachment == null) {
  105. return res.json(ApiResponse.error('attachment not found'));
  106. }
  107. const user = req.user;
  108. const isAccessible = await isAccessibleByViewer(user, attachment);
  109. if (!isAccessible) {
  110. return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'`));
  111. }
  112. // add headers before evaluating 'req.fresh'
  113. setHeaderToRes(res, attachment, forceDownload);
  114. // return 304 if request is "fresh"
  115. // see: http://expressjs.com/en/5x/api.html#req.fresh
  116. if (req.fresh) {
  117. return res.sendStatus(304);
  118. }
  119. let fileStream;
  120. try {
  121. fileStream = await fileUploader.findDeliveryFile(attachment);
  122. }
  123. catch (e) {
  124. logger.error(e);
  125. return res.json(ApiResponse.error(e.message));
  126. }
  127. return fileStream.pipe(res);
  128. }
  129. /**
  130. * set http response header
  131. *
  132. * @param {Response} res
  133. * @param {Attachment} attachment
  134. * @param {boolean} forceDownload
  135. */
  136. function setHeaderToRes(res, attachment, forceDownload) {
  137. res.set({
  138. ETag: `Attachment-${attachment._id}`,
  139. 'Last-Modified': attachment.createdAt,
  140. });
  141. // download
  142. if (forceDownload) {
  143. res.set({
  144. 'Content-Disposition': `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
  145. });
  146. }
  147. // reference
  148. else {
  149. res.set('Content-Type', attachment.fileFormat);
  150. }
  151. }
  152. async function createAttachment(file, user, pageId = null) {
  153. // check limit
  154. const res = await fileUploader.checkLimit(file.size);
  155. if (!res.isUploadable) {
  156. throw new Error(res.errorMessage);
  157. }
  158. const fileStream = fs.createReadStream(file.path, {
  159. flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true,
  160. });
  161. // create an Attachment document and upload file
  162. let attachment;
  163. try {
  164. attachment = await Attachment.create(pageId, user, fileStream, file.originalname, file.mimetype, file.size);
  165. }
  166. catch (err) {
  167. // delete temporary file
  168. fs.unlink(file.path, (err) => { if (err) { logger.error('Error while deleting tmp file.') } });
  169. throw err;
  170. }
  171. return attachment;
  172. }
  173. const actions = {};
  174. const api = {};
  175. actions.api = api;
  176. api.download = async function(req, res) {
  177. const id = req.params.id;
  178. const attachment = await Attachment.findById(id);
  179. return responseForAttachment(req, res, attachment, true);
  180. };
  181. /**
  182. * @api {get} /attachments.get get attachments
  183. * @apiName get
  184. * @apiGroup Attachment
  185. *
  186. * @apiParam {String} id
  187. */
  188. api.get = async function(req, res) {
  189. const id = req.params.id;
  190. const attachment = await Attachment.findById(id);
  191. return responseForAttachment(req, res, attachment);
  192. };
  193. /**
  194. * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
  195. * @apiName get
  196. * @apiGroup Attachment
  197. *
  198. * @apiParam {String} pageId, fileName
  199. */
  200. api.obsoletedGetForMongoDB = async function(req, res) {
  201. if (process.env.FILE_UPLOAD !== 'mongodb') {
  202. return res.status(400);
  203. }
  204. const pageId = req.params.pageId;
  205. const fileName = req.params.fileName;
  206. const filePath = `attachment/${pageId}/${fileName}`;
  207. const attachment = await Attachment.findOne({ filePath });
  208. return responseForAttachment(req, res, attachment);
  209. };
  210. /**
  211. * @swagger
  212. *
  213. * /_api/attachments.list:
  214. * get:
  215. * tags: [Attachments, apiv1]
  216. * operationId: listAttachments
  217. * summary: /_api/attachments.list
  218. * description: Get list of attachments in page
  219. * parameters:
  220. * - in: query
  221. * name: page_id
  222. * schema:
  223. * $ref: '#/components/schemas/Page/properties/_id'
  224. * required: true
  225. * responses:
  226. * 200:
  227. * description: Succeeded to get list of attachments.
  228. * content:
  229. * application/json:
  230. * schema:
  231. * properties:
  232. * ok:
  233. * $ref: '#/components/schemas/V1Response/properties/ok'
  234. * attachments:
  235. * type: array
  236. * items:
  237. * $ref: '#/components/schemas/Attachment'
  238. * description: attachment list
  239. * 403:
  240. * $ref: '#/components/responses/403'
  241. * 500:
  242. * $ref: '#/components/responses/500'
  243. */
  244. /**
  245. * @api {get} /attachments.list Get attachments of the page
  246. * @apiName ListAttachments
  247. * @apiGroup Attachment
  248. *
  249. * @apiParam {String} page_id
  250. */
  251. api.list = async function(req, res) {
  252. const id = req.query.page_id || null;
  253. if (!id) {
  254. return res.json(ApiResponse.error('Parameters page_id is required.'));
  255. }
  256. let attachments = await Attachment.find({ page: id })
  257. .sort({ updatedAt: 1 })
  258. .populate({ path: 'creator', select: User.USER_PUBLIC_FIELDS, populate: User.IMAGE_POPULATION });
  259. attachments = attachments.map((attachment) => {
  260. return attachment.toObject({ virtuals: true });
  261. });
  262. return res.json(ApiResponse.success({ attachments }));
  263. };
  264. /**
  265. * @api {get} /attachments.limit get available capacity of uploaded file with GridFS
  266. * @apiName AddAttachments
  267. * @apiGroup Attachment
  268. */
  269. api.limit = async function(req, res) {
  270. const fileSize = Number(req.query.fileSize);
  271. return res.json(ApiResponse.success(await fileUploader.checkLimit(fileSize)));
  272. };
  273. /**
  274. * @swagger
  275. *
  276. * /_api/attachments.add:
  277. * post:
  278. * tags: [Attachments, apiv1]
  279. * operationId: addAttachment
  280. * summary: /_api/attachments.add
  281. * description: Add attachment to the page
  282. * requestBody:
  283. * content:
  284. * "multipart/form-data":
  285. * schema:
  286. * properties:
  287. * page_id:
  288. * nullable: true
  289. * type: string
  290. * path:
  291. * nullable: true
  292. * type: string
  293. * file:
  294. * type: string
  295. * format: binary
  296. * description: attachment data
  297. * encoding:
  298. * path:
  299. * contentType: application/x-www-form-urlencoded
  300. * "*\/*":
  301. * schema:
  302. * properties:
  303. * page_id:
  304. * nullable: true
  305. * type: string
  306. * path:
  307. * nullable: true
  308. * type: string
  309. * file:
  310. * type: string
  311. * format: binary
  312. * description: attachment data
  313. * encoding:
  314. * path:
  315. * contentType: application/x-www-form-urlencoded
  316. * responses:
  317. * 200:
  318. * description: Succeeded to add attachment.
  319. * content:
  320. * application/json:
  321. * schema:
  322. * properties:
  323. * ok:
  324. * $ref: '#/components/schemas/V1Response/properties/ok'
  325. * page:
  326. * $ref: '#/components/schemas/Page'
  327. * attachment:
  328. * $ref: '#/components/schemas/Attachment'
  329. * url:
  330. * $ref: '#/components/schemas/Attachment/properties/url'
  331. * pageCreated:
  332. * type: boolean
  333. * description: whether the page was created
  334. * example: false
  335. * 403:
  336. * $ref: '#/components/responses/403'
  337. * 500:
  338. * $ref: '#/components/responses/500'
  339. */
  340. /**
  341. * @api {post} /attachments.add Add attachment to the page
  342. * @apiName AddAttachments
  343. * @apiGroup Attachment
  344. *
  345. * @apiParam {String} page_id
  346. * @apiParam {File} file
  347. */
  348. api.add = async function(req, res) {
  349. let pageId = req.body.page_id || null;
  350. const pagePath = decodeURIComponent(req.body.path) || null;
  351. let pageCreated = false;
  352. // check params
  353. if (pageId == null && pagePath == null) {
  354. return res.json(ApiResponse.error('Either page_id or path is required.'));
  355. }
  356. if (!req.file) {
  357. return res.json(ApiResponse.error('File error.'));
  358. }
  359. const file = req.file;
  360. let page;
  361. if (pageId == null) {
  362. logger.debug('Create page before file upload');
  363. page = await Page.create(pagePath, `# ${pagePath}`, req.user, { grant: Page.GRANT_OWNER });
  364. pageCreated = true;
  365. pageId = page._id;
  366. }
  367. else {
  368. page = await Page.findById(pageId);
  369. // check the user is accessible
  370. const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
  371. if (!isAccessible) {
  372. return res.json(ApiResponse.error(`Forbidden to access to the page '${page.id}'`));
  373. }
  374. }
  375. let attachment;
  376. try {
  377. attachment = await createAttachment(file, req.user, pageId);
  378. }
  379. catch (err) {
  380. logger.error(err);
  381. return res.json(ApiResponse.error(err.message));
  382. }
  383. const result = {
  384. page: page.toObject(),
  385. attachment: attachment.toObject({ virtuals: true }),
  386. pageCreated,
  387. };
  388. return res.json(ApiResponse.success(result));
  389. };
  390. /**
  391. * @api {post} /attachments.uploadProfileImage Add attachment for profile image
  392. * @apiName UploadProfileImage
  393. * @apiGroup Attachment
  394. *
  395. * @apiParam {File} file
  396. */
  397. api.uploadProfileImage = async function(req, res) {
  398. // check params
  399. if (!req.file) {
  400. return res.json(ApiResponse.error('File error.'));
  401. }
  402. if (!req.user) {
  403. return res.json(ApiResponse.error('param "user" must be set.'));
  404. }
  405. const file = req.file;
  406. // check type
  407. const acceptableFileType = /image\/.+/;
  408. if (!file.mimetype.match(acceptableFileType)) {
  409. return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
  410. }
  411. let attachment;
  412. try {
  413. req.user.deleteImage();
  414. attachment = await createAttachment(file, req.user);
  415. await req.user.updateImage(attachment);
  416. }
  417. catch (err) {
  418. logger.error(err);
  419. return res.json(ApiResponse.error(err.message));
  420. }
  421. const result = {
  422. attachment: attachment.toObject({ virtuals: true }),
  423. };
  424. return res.json(ApiResponse.success(result));
  425. };
  426. /**
  427. * @swagger
  428. *
  429. * /_api/attachments.remove:
  430. * post:
  431. * tags: [Attachments, apiv1]
  432. * operationId: removeAttachment
  433. * summary: /_api/attachments.remove
  434. * description: Remove attachment
  435. * requestBody:
  436. * content:
  437. * application/json:
  438. * schema:
  439. * properties:
  440. * attachment_id:
  441. * $ref: '#/components/schemas/Attachment/properties/_id'
  442. * required:
  443. * - attachment_id
  444. * responses:
  445. * 200:
  446. * description: Succeeded to remove attachment.
  447. * content:
  448. * application/json:
  449. * schema:
  450. * properties:
  451. * ok:
  452. * $ref: '#/components/schemas/V1Response/properties/ok'
  453. * 403:
  454. * $ref: '#/components/responses/403'
  455. * 500:
  456. * $ref: '#/components/responses/500'
  457. */
  458. /**
  459. * @api {post} /attachments.remove Remove attachments
  460. * @apiName RemoveAttachments
  461. * @apiGroup Attachment
  462. *
  463. * @apiParam {String} attachment_id
  464. */
  465. api.remove = async function(req, res) {
  466. const id = req.body.attachment_id;
  467. const attachment = await Attachment.findById(id);
  468. if (attachment == null) {
  469. return res.json(ApiResponse.error('attachment not found'));
  470. }
  471. const isDeletable = await isDeletableByUser(req.user, attachment);
  472. if (!isDeletable) {
  473. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  474. }
  475. try {
  476. await Attachment.removeWithSubstanceById(id);
  477. }
  478. catch (err) {
  479. logger.error(err);
  480. return res.status(500).json(ApiResponse.error('Error while deleting file'));
  481. }
  482. return res.json(ApiResponse.success({}));
  483. };
  484. /**
  485. * @api {post} /attachments.removeProfileImage Remove profile image attachments
  486. * @apiGroup Attachment
  487. * @apiParam {String} attachment_id
  488. */
  489. api.removeProfileImage = async function(req, res) {
  490. const user = req.user;
  491. const attachment = await Attachment.findById(user.imageAttachment);
  492. if (attachment == null) {
  493. return res.json(ApiResponse.error('attachment not found'));
  494. }
  495. const isDeletable = await isDeletableByUser(user, attachment);
  496. if (!isDeletable) {
  497. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  498. }
  499. try {
  500. await user.deleteImage();
  501. }
  502. catch (err) {
  503. logger.error(err);
  504. return res.status(500).json(ApiResponse.error('Error while deleting image'));
  505. }
  506. return res.json(ApiResponse.success({}));
  507. };
  508. return actions;
  509. };