attachment.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. const debug = require('debug')('growi:routss:attachment');
  2. const logger = require('@alias/logger')('growi:routes:attachment');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const ApiResponse = require('../util/apiResponse');
  6. module.exports = function(crowi, app) {
  7. const Attachment = crowi.model('Attachment');
  8. const User = crowi.model('User');
  9. const Page = crowi.model('Page');
  10. const fileUploader = require('../service/file-uploader')(crowi, app);
  11. /**
  12. * Common method to response
  13. *
  14. * @param {Response} res
  15. * @param {Attachment} attachment
  16. * @param {boolean} forceDownload
  17. */
  18. async function responseForAttachment(res, attachment, forceDownload) {
  19. let fileStream;
  20. try {
  21. fileStream = await fileUploader.findDeliveryFile(attachment);
  22. }
  23. catch (e) {
  24. logger.error(e);
  25. return res.json(ApiResponse.error(e.message));
  26. }
  27. setHeaderToRes(res, attachment, forceDownload);
  28. return fileStream.pipe(res);
  29. }
  30. /**
  31. * set http response header
  32. *
  33. * @param {Response} res
  34. * @param {Attachment} attachment
  35. * @param {boolean} forceDownload
  36. */
  37. function setHeaderToRes(res, attachment, forceDownload) {
  38. // download
  39. if (forceDownload) {
  40. const headers = {
  41. 'Content-Type': 'application/force-download',
  42. 'Content-Disposition': `inline;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
  43. };
  44. res.writeHead(200, headers);
  45. }
  46. // reference
  47. else {
  48. res.set('Content-Type', attachment.fileFormat);
  49. }
  50. }
  51. async function createAttachment(file, user, pageId = null) {
  52. // check capacity
  53. const isUploadable = await fileUploader.checkCapacity(file.size);
  54. if (!isUploadable) {
  55. throw new Error('File storage reaches limit');
  56. }
  57. const fileStream = fs.createReadStream(file.path, {flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true });
  58. // create an Attachment document and upload file
  59. let attachment;
  60. try {
  61. attachment = await Attachment.create(pageId, user, fileStream, file.originalname, file.mimetype, file.size);
  62. }
  63. catch (err) {
  64. // delete temporary file
  65. fs.unlink(file.path, function(err) { if (err) { logger.error('Error while deleting tmp file.') } });
  66. throw err;
  67. }
  68. return attachment;
  69. }
  70. const actions = {};
  71. const api = {};
  72. actions.api = api;
  73. api.download = async function(req, res) {
  74. const id = req.params.id;
  75. const attachment = await Attachment.findById(id);
  76. if (attachment == null) {
  77. return res.json(ApiResponse.error('attachment not found'));
  78. }
  79. // TODO for GC-1359: consider restriction
  80. return responseForAttachment(res, attachment, true);
  81. };
  82. /**
  83. * @api {get} /attachments.get get attachments
  84. * @apiName get
  85. * @apiGroup Attachment
  86. *
  87. * @apiParam {String} id
  88. */
  89. api.get = async function(req, res) {
  90. const id = req.params.id;
  91. const attachment = await Attachment.findById(id);
  92. if (attachment == null) {
  93. return res.json(ApiResponse.error('attachment not found'));
  94. }
  95. // TODO for GC-1359: consider restriction
  96. return responseForAttachment(res, attachment);
  97. };
  98. /**
  99. * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
  100. * @apiName get
  101. * @apiGroup Attachment
  102. *
  103. * @apiParam {String} pageId, fileName
  104. */
  105. api.obsoletedGetForMongoDB = async function(req, res) {
  106. if (process.env.FILE_UPLOAD !== 'mongodb') {
  107. return res.status(400);
  108. }
  109. const pageId = req.params.pageId;
  110. const fileName = req.params.fileName;
  111. const filePath = `attachment/${pageId}/${fileName}`;
  112. const attachment = await Attachment.findOne({ filePath });
  113. if (attachment == null) {
  114. return res.json(ApiResponse.error('attachment not found'));
  115. }
  116. // TODO for GC-1359: consider restriction
  117. return responseForAttachment(res, attachment);
  118. };
  119. /**
  120. * @api {get} /attachments.list Get attachments of the page
  121. * @apiName ListAttachments
  122. * @apiGroup Attachment
  123. *
  124. * @apiParam {String} page_id
  125. */
  126. api.list = async function(req, res) {
  127. const id = req.query.page_id || null;
  128. if (!id) {
  129. return res.json(ApiResponse.error('Parameters page_id is required.'));
  130. }
  131. let attachments = await Attachment.find({page: id})
  132. .sort({'updatedAt': 1})
  133. .populate('creator', User.USER_PUBLIC_FIELDS);
  134. attachments = attachments.map(attachment => attachment.toObject({ virtuals: true }));
  135. return res.json(ApiResponse.success({ attachments }));
  136. };
  137. /**
  138. * @api {get} /attachments.limit get available capacity of uploaded file with GridFS
  139. * @apiName AddAttachments
  140. * @apiGroup Attachment
  141. */
  142. api.limit = async function(req, res) {
  143. const isUploadable = await fileUploader.checkCapacity(req.query.fileSize);
  144. return res.json(ApiResponse.success({isUploadable: isUploadable}));
  145. };
  146. /**
  147. * @api {post} /attachments.add Add attachment to the page
  148. * @apiName AddAttachments
  149. * @apiGroup Attachment
  150. *
  151. * @apiParam {String} page_id
  152. * @apiParam {File} file
  153. */
  154. api.add = async function(req, res) {
  155. let pageId = req.body.page_id || null;
  156. const pagePath = decodeURIComponent(req.body.path) || null;
  157. let pageCreated = false;
  158. // check params
  159. if (pageId == null && pagePath == null) {
  160. return res.json(ApiResponse.error('Either page_id or path is required.'));
  161. }
  162. if (!req.file) {
  163. return res.json(ApiResponse.error('File error.'));
  164. }
  165. const file = req.file;
  166. // TODO for GC-1359: consider restriction
  167. let page;
  168. if (pageId == null) {
  169. logger.debug('Create page before file upload');
  170. page = await Page.create(path, '# ' + path, req.user, {grant: Page.GRANT_OWNER});
  171. pageCreated = true;
  172. pageId = page._id;
  173. }
  174. else {
  175. page = await Page.findById(pageId);
  176. }
  177. let attachment;
  178. try {
  179. attachment = await createAttachment(file, req.user, pageId);
  180. }
  181. catch (err) {
  182. logger.error(err);
  183. return res.json(ApiResponse.error(err.message));
  184. }
  185. const result = {
  186. page: page.toObject(),
  187. attachment: attachment.toObject({ virtuals: true }),
  188. pageCreated: pageCreated,
  189. };
  190. return res.json(ApiResponse.success(result));
  191. };
  192. /**
  193. * @api {post} /attachments.uploadProfileImage Add attachment for profile image
  194. * @apiName UploadProfileImage
  195. * @apiGroup Attachment
  196. *
  197. * @apiParam {File} file
  198. */
  199. api.uploadProfileImage = async function(req, res) {
  200. // check params
  201. if (!req.file) {
  202. return res.json(ApiResponse.error('File error.'));
  203. }
  204. const file = req.file;
  205. // check type
  206. const acceptableFileType = /image\/.+/;
  207. if (!file.mimetype.match(acceptableFileType)) {
  208. return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
  209. }
  210. let attachment;
  211. try {
  212. attachment = await createAttachment(file, req.user);
  213. }
  214. catch (err) {
  215. logger.error(err);
  216. return res.json(ApiResponse.error(err.message));
  217. }
  218. const result = {
  219. attachment: attachment.toObject({ virtuals: true }),
  220. };
  221. return res.json(ApiResponse.success(result));
  222. };
  223. /**
  224. * @api {post} /attachments.remove Remove attachments
  225. * @apiName RemoveAttachments
  226. * @apiGroup Attachment
  227. *
  228. * @apiParam {String} attachment_id
  229. */
  230. api.remove = function(req, res) {
  231. const id = req.body.attachment_id;
  232. Attachment.findById(id)
  233. .then(function(data) {
  234. const attachment = data;
  235. attachment.removeWithSubstance()
  236. .then(data => {
  237. debug('removeAttachment', data);
  238. return res.json(ApiResponse.success({}));
  239. }).catch(err => {
  240. logger.error('Error', err);
  241. return res.status(500).json(ApiResponse.error('Error while deleting file'));
  242. });
  243. }).catch(err => {
  244. logger.error('Error', err);
  245. return res.status(404);
  246. });
  247. };
  248. return actions;
  249. };