get.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import {
  2. getIdStringForRef, type IPage, type IUser,
  3. } from '@growi/core';
  4. import express from 'express';
  5. import type {
  6. NextFunction, Request, Response, Router,
  7. } from 'express';
  8. import mongoose from 'mongoose';
  9. import type { CrowiProperties, CrowiRequest } from '~/interfaces/crowi-request';
  10. import { ResponseMode, type ExpressHttpHeader, type RespondOptions } from '~/server/interfaces/attachment';
  11. import {
  12. type FileUploader,
  13. toExpressHttpHeaders, ContentHeaders, applyHeaders,
  14. } from '~/server/service/file-uploader';
  15. import loggerFactory from '~/utils/logger';
  16. import type Crowi from '../../crowi';
  17. import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
  18. import { Attachment, type IAttachmentDocument } from '../../models/attachment';
  19. import ApiResponse from '../../util/apiResponse';
  20. const logger = loggerFactory('growi:routes:attachment:get');
  21. // TODO: remove this local interface when models/page has typescriptized
  22. interface PageModel {
  23. isAccessiblePageByViewer: (pageId: string, user: IUser | undefined) => Promise<boolean>
  24. }
  25. type LocalsAfterDataInjection = { attachment: IAttachmentDocument };
  26. type RetrieveAttachmentFromIdParamRequest = CrowiProperties & Request<
  27. { id: string },
  28. any, any, any,
  29. LocalsAfterDataInjection
  30. >;
  31. type RetrieveAttachmentFromIdParamResponse = Response<
  32. any,
  33. LocalsAfterDataInjection
  34. >;
  35. export const retrieveAttachmentFromIdParam = async(
  36. req: RetrieveAttachmentFromIdParamRequest, res: RetrieveAttachmentFromIdParamResponse, next: NextFunction,
  37. ): Promise<void> => {
  38. const id = req.params.id;
  39. const attachment = await Attachment.findById(id);
  40. if (attachment == null) {
  41. res.json(ApiResponse.error('attachment not found'));
  42. return;
  43. }
  44. const user = req.user;
  45. // check viewer has permission
  46. if (user != null && attachment.page != null) {
  47. const Page = mongoose.model<IPage, PageModel>('Page');
  48. const isAccessible = await Page.isAccessiblePageByViewer(getIdStringForRef(attachment.page), user);
  49. if (!isAccessible) {
  50. res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'. This attachment might belong to other pages.`));
  51. return;
  52. }
  53. }
  54. res.locals.attachment = attachment;
  55. return next();
  56. };
  57. export const generateHeadersForFresh = (attachment: IAttachmentDocument): ExpressHttpHeader[] => {
  58. return toExpressHttpHeaders({
  59. ETag: `Attachment-${attachment._id}`,
  60. 'Last-Modified': attachment.createdAt.toUTCString(),
  61. });
  62. };
  63. const respondForRedirectMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
  64. const isDownload = opts?.download ?? false;
  65. if (!isDownload) {
  66. const temporaryUrl = attachment.getValidTemporaryUrl();
  67. if (temporaryUrl != null) {
  68. res.redirect(temporaryUrl);
  69. return;
  70. }
  71. }
  72. const temporaryUrl = await fileUploadService.generateTemporaryUrl(attachment, opts);
  73. res.redirect(temporaryUrl.url);
  74. // persist temporaryUrl
  75. if (!isDownload) {
  76. try {
  77. attachment.cashTemporaryUrlByProvideSec(temporaryUrl.url, temporaryUrl.lifetimeSec);
  78. return;
  79. }
  80. catch (err) {
  81. logger.error(err);
  82. }
  83. }
  84. };
  85. const respondForRelayMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
  86. // apply content-* headers before response
  87. const isDownload = opts?.download ?? false;
  88. const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
  89. applyHeaders(res, contentHeaders.toExpressHttpHeaders());
  90. try {
  91. const readable = await fileUploadService.findDeliveryFile(attachment);
  92. readable.pipe(res);
  93. }
  94. catch (e) {
  95. logger.error(e);
  96. res.json(ApiResponse.error(e.message));
  97. return;
  98. }
  99. };
  100. export const getActionFactory = (crowi: Crowi, attachment: IAttachmentDocument) => {
  101. return async(req: CrowiRequest, res: Response, opts?: RespondOptions): Promise<void> => {
  102. // add headers before evaluating 'req.fresh'
  103. applyHeaders(res, generateHeadersForFresh(attachment));
  104. // return 304 if request is "fresh"
  105. // see: http://expressjs.com/en/5x/api.html#req.fresh
  106. if (req.fresh) {
  107. res.sendStatus(304);
  108. return;
  109. }
  110. const { fileUploadService } = crowi;
  111. const responseMode = fileUploadService.determineResponseMode();
  112. switch (responseMode) {
  113. case ResponseMode.DELEGATE:
  114. fileUploadService.respond(res, attachment, opts);
  115. return;
  116. case ResponseMode.REDIRECT:
  117. respondForRedirectMode(res, fileUploadService, attachment, opts);
  118. return;
  119. case ResponseMode.RELAY:
  120. respondForRelayMode(res, fileUploadService, attachment, opts);
  121. return;
  122. }
  123. };
  124. };
  125. export type GetRequest = CrowiProperties & Request<
  126. { id: string },
  127. any, any, any,
  128. LocalsAfterDataInjection
  129. >;
  130. export type GetResponse = Response<
  131. any,
  132. LocalsAfterDataInjection
  133. >
  134. export const getRouterFactory = (crowi: Crowi): Router => {
  135. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  136. const router = express.Router();
  137. // note: retrieveAttachmentFromIdParam requires `req.params.id`
  138. // TODO: https://redmine.weseek.co.jp/issues/166911
  139. router.get<{ id: string }>('/:id([0-9a-z]{24})',
  140. certifySharedPageAttachmentMiddleware,
  141. loginRequired,
  142. retrieveAttachmentFromIdParam,
  143. (req: GetRequest, res: GetResponse) => {
  144. const { attachment } = res.locals;
  145. const getAction = getActionFactory(crowi, attachment);
  146. getAction(req, res);
  147. });
  148. return router;
  149. };