attachment.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. module.exports = function(crowi, app) {
  6. const Attachment = crowi.model('Attachment');
  7. const User = crowi.model('User');
  8. const Page = crowi.model('Page');
  9. const fileUploader = require('../service/file-uploader')(crowi, app);
  10. /**
  11. * Check the user is accessible to the related page
  12. *
  13. * @param {User} user
  14. * @param {Attachment} attachment
  15. */
  16. async function isAccessibleByViewer(user, attachment) {
  17. if (attachment.page != null) {
  18. // eslint-disable-next-line no-return-await
  19. return await Page.isAccessiblePageByViewer(attachment.page, user);
  20. }
  21. return true;
  22. }
  23. /**
  24. * Check the user is accessible to the related page
  25. *
  26. * @param {User} user
  27. * @param {Attachment} attachment
  28. */
  29. async function isDeletableByUser(user, attachment) {
  30. const ownerId = attachment.creator._id || attachment.creator;
  31. if (attachment.page == null) { // when profile image
  32. return user.id === ownerId.toString();
  33. }
  34. // eslint-disable-next-line no-return-await
  35. return await Page.isAccessiblePageByViewer(attachment.page, user);
  36. }
  37. /**
  38. * Common method to response
  39. *
  40. * @param {Request} req
  41. * @param {Response} res
  42. * @param {User} user
  43. * @param {Attachment} attachment
  44. * @param {boolean} forceDownload
  45. */
  46. async function responseForAttachment(req, res, attachment, forceDownload) {
  47. if (attachment == null) {
  48. return res.json(ApiResponse.error('attachment not found'));
  49. }
  50. const user = req.user;
  51. const isAccessible = await isAccessibleByViewer(user, attachment);
  52. if (!isAccessible) {
  53. return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'`));
  54. }
  55. // add headers before evaluating 'req.fresh'
  56. setHeaderToRes(res, attachment, forceDownload);
  57. // return 304 if request is "fresh"
  58. // see: http://expressjs.com/en/5x/api.html#req.fresh
  59. if (req.fresh) {
  60. return res.sendStatus(304);
  61. }
  62. let fileStream;
  63. try {
  64. fileStream = await fileUploader.findDeliveryFile(attachment);
  65. }
  66. catch (e) {
  67. logger.error(e);
  68. return res.json(ApiResponse.error(e.message));
  69. }
  70. return fileStream.pipe(res);
  71. }
  72. /**
  73. * set http response header
  74. *
  75. * @param {Response} res
  76. * @param {Attachment} attachment
  77. * @param {boolean} forceDownload
  78. */
  79. function setHeaderToRes(res, attachment, forceDownload) {
  80. res.set({
  81. ETag: `Attachment-${attachment._id}`,
  82. 'Last-Modified': attachment.createdAt,
  83. });
  84. // download
  85. if (forceDownload) {
  86. res.set({
  87. 'Content-Disposition': `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
  88. });
  89. }
  90. // reference
  91. else {
  92. res.set('Content-Type', attachment.fileFormat);
  93. }
  94. }
  95. async function createAttachment(file, user, pageId = null) {
  96. // check limit
  97. const res = await fileUploader.checkLimit(file.size);
  98. if (!res.isUploadable) {
  99. throw new Error(res.errorMessage);
  100. }
  101. const fileStream = fs.createReadStream(file.path, {
  102. flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true,
  103. });
  104. // create an Attachment document and upload file
  105. let attachment;
  106. try {
  107. attachment = await Attachment.create(pageId, user, fileStream, file.originalname, file.mimetype, file.size);
  108. }
  109. catch (err) {
  110. // delete temporary file
  111. fs.unlink(file.path, (err) => { if (err) { logger.error('Error while deleting tmp file.') } });
  112. throw err;
  113. }
  114. return attachment;
  115. }
  116. const actions = {};
  117. const api = {};
  118. actions.api = api;
  119. api.download = async function(req, res) {
  120. const id = req.params.id;
  121. const attachment = await Attachment.findById(id);
  122. return responseForAttachment(req, res, attachment, true);
  123. };
  124. /**
  125. * @api {get} /attachments.get get attachments
  126. * @apiName get
  127. * @apiGroup Attachment
  128. *
  129. * @apiParam {String} id
  130. */
  131. api.get = async function(req, res) {
  132. const id = req.params.id;
  133. const attachment = await Attachment.findById(id);
  134. return responseForAttachment(req, res, attachment);
  135. };
  136. /**
  137. * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
  138. * @apiName get
  139. * @apiGroup Attachment
  140. *
  141. * @apiParam {String} pageId, fileName
  142. */
  143. api.obsoletedGetForMongoDB = async function(req, res) {
  144. if (process.env.FILE_UPLOAD !== 'mongodb') {
  145. return res.status(400);
  146. }
  147. const pageId = req.params.pageId;
  148. const fileName = req.params.fileName;
  149. const filePath = `attachment/${pageId}/${fileName}`;
  150. const attachment = await Attachment.findOne({ filePath });
  151. return responseForAttachment(req, res, attachment);
  152. };
  153. /**
  154. * @api {get} /attachments.list Get attachments of the page
  155. * @apiName ListAttachments
  156. * @apiGroup Attachment
  157. *
  158. * @apiParam {String} page_id
  159. */
  160. api.list = async function(req, res) {
  161. const id = req.query.page_id || null;
  162. if (!id) {
  163. return res.json(ApiResponse.error('Parameters page_id is required.'));
  164. }
  165. let attachments = await Attachment.find({ page: id })
  166. .sort({ updatedAt: 1 })
  167. .populate({ path: 'creator', select: User.USER_PUBLIC_FIELDS, populate: User.IMAGE_POPULATION });
  168. attachments = attachments.map((attachment) => {
  169. return attachment.toObject({ virtuals: true });
  170. });
  171. return res.json(ApiResponse.success({ attachments }));
  172. };
  173. /**
  174. * @api {get} /attachments.limit get available capacity of uploaded file with GridFS
  175. * @apiName AddAttachments
  176. * @apiGroup Attachment
  177. */
  178. api.limit = async function(req, res) {
  179. const fileSize = Number(req.query.fileSize);
  180. return res.json(ApiResponse.success(await fileUploader.checkLimit(fileSize)));
  181. };
  182. /**
  183. * @api {post} /attachments.add Add attachment to the page
  184. * @apiName AddAttachments
  185. * @apiGroup Attachment
  186. *
  187. * @apiParam {String} page_id
  188. * @apiParam {File} file
  189. */
  190. api.add = async function(req, res) {
  191. let pageId = req.body.page_id || null;
  192. const pagePath = decodeURIComponent(req.body.path) || null;
  193. let pageCreated = false;
  194. // check params
  195. if (pageId == null && pagePath == null) {
  196. return res.json(ApiResponse.error('Either page_id or path is required.'));
  197. }
  198. if (!req.file) {
  199. return res.json(ApiResponse.error('File error.'));
  200. }
  201. const file = req.file;
  202. let page;
  203. if (pageId == null) {
  204. logger.debug('Create page before file upload');
  205. page = await Page.create(pagePath, `# ${pagePath}`, req.user, { grant: Page.GRANT_OWNER });
  206. pageCreated = true;
  207. pageId = page._id;
  208. }
  209. else {
  210. page = await Page.findById(pageId);
  211. // check the user is accessible
  212. const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
  213. if (!isAccessible) {
  214. return res.json(ApiResponse.error(`Forbidden to access to the page '${page.id}'`));
  215. }
  216. }
  217. let attachment;
  218. try {
  219. attachment = await createAttachment(file, req.user, pageId);
  220. }
  221. catch (err) {
  222. logger.error(err);
  223. return res.json(ApiResponse.error(err.message));
  224. }
  225. const result = {
  226. page: page.toObject(),
  227. attachment: attachment.toObject({ virtuals: true }),
  228. pageCreated,
  229. };
  230. return res.json(ApiResponse.success(result));
  231. };
  232. /**
  233. * @api {post} /attachments.uploadProfileImage Add attachment for profile image
  234. * @apiName UploadProfileImage
  235. * @apiGroup Attachment
  236. *
  237. * @apiParam {File} file
  238. */
  239. api.uploadProfileImage = async function(req, res) {
  240. // check params
  241. if (!req.file) {
  242. return res.json(ApiResponse.error('File error.'));
  243. }
  244. if (!req.user) {
  245. return res.json(ApiResponse.error('param "user" must be set.'));
  246. }
  247. const file = req.file;
  248. // check type
  249. const acceptableFileType = /image\/.+/;
  250. if (!file.mimetype.match(acceptableFileType)) {
  251. return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
  252. }
  253. let attachment;
  254. try {
  255. req.user.deleteImage();
  256. attachment = await createAttachment(file, req.user);
  257. await req.user.updateImage(attachment);
  258. }
  259. catch (err) {
  260. logger.error(err);
  261. return res.json(ApiResponse.error(err.message));
  262. }
  263. const result = {
  264. attachment: attachment.toObject({ virtuals: true }),
  265. };
  266. return res.json(ApiResponse.success(result));
  267. };
  268. /**
  269. * @api {post} /attachments.remove Remove attachments
  270. * @apiName RemoveAttachments
  271. * @apiGroup Attachment
  272. *
  273. * @apiParam {String} attachment_id
  274. */
  275. api.remove = async function(req, res) {
  276. const id = req.body.attachment_id;
  277. const attachment = await Attachment.findById(id);
  278. if (attachment == null) {
  279. return res.json(ApiResponse.error('attachment not found'));
  280. }
  281. const isDeletable = await isDeletableByUser(req.user, attachment);
  282. if (!isDeletable) {
  283. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  284. }
  285. try {
  286. await Attachment.removeWithSubstanceById(id);
  287. }
  288. catch (err) {
  289. logger.error(err);
  290. return res.status(500).json(ApiResponse.error('Error while deleting file'));
  291. }
  292. return res.json(ApiResponse.success({}));
  293. };
  294. /**
  295. * @api {post} /attachments.removeProfileImage Remove profile image attachments
  296. * @apiGroup Attachment
  297. * @apiParam {String} attachment_id
  298. */
  299. api.removeProfileImage = async function(req, res) {
  300. const user = req.user;
  301. const attachment = await Attachment.findById(user.imageAttachment);
  302. if (attachment == null) {
  303. return res.json(ApiResponse.error('attachment not found'));
  304. }
  305. const isDeletable = await isDeletableByUser(user, attachment);
  306. if (!isDeletable) {
  307. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  308. }
  309. try {
  310. await user.deleteImage();
  311. }
  312. catch (err) {
  313. logger.error(err);
  314. return res.status(500).json(ApiResponse.error('Error while deleting image'));
  315. }
  316. return res.json(ApiResponse.success({}));
  317. };
  318. return actions;
  319. };