attachment.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import type { IAttachment } from '@growi/core';
  2. import { addSeconds } from 'date-fns/addSeconds';
  3. import { type Document, type Model, Schema } from 'mongoose';
  4. import mongoosePaginate from 'mongoose-paginate-v2';
  5. import uniqueValidator from 'mongoose-unique-validator';
  6. import path from 'path';
  7. import loggerFactory from '~/utils/logger';
  8. import { AttachmentType } from '../interfaces/attachment';
  9. import { getOrCreateModel } from '../util/mongoose-utils';
  10. // eslint-disable-next-line no-unused-vars
  11. const logger = loggerFactory('growi:models:attachment');
  12. function generateFileHash(fileName) {
  13. const hash = require('crypto').createHash('md5');
  14. hash.update(`${fileName}_${Date.now()}`);
  15. return hash.digest('hex');
  16. }
  17. type GetValidTemporaryUrl = () => string | null | undefined;
  18. type CashTemporaryUrlByProvideSec = (
  19. temporaryUrl: string,
  20. lifetimeSec: number,
  21. ) => Promise<IAttachmentDocument>;
  22. export interface IAttachmentDocument extends IAttachment, Document {
  23. getValidTemporaryUrl: GetValidTemporaryUrl;
  24. cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec;
  25. }
  26. export interface IAttachmentModel extends Model<IAttachmentDocument> {
  27. createWithoutSave: (
  28. pageId,
  29. user,
  30. originalName: string,
  31. fileFormat: string,
  32. fileSize: number,
  33. attachmentType: AttachmentType,
  34. ) => IAttachmentDocument;
  35. }
  36. const attachmentSchema = new Schema(
  37. {
  38. page: { type: Schema.Types.ObjectId, ref: 'Page', index: true },
  39. creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
  40. filePath: { type: String }, // DEPRECATED: remains for backward compatibility for v3.3.x or below
  41. fileName: { type: String, required: true, unique: true },
  42. fileFormat: { type: String, required: true },
  43. fileSize: { type: Number, default: 0 },
  44. originalName: { type: String },
  45. temporaryUrlCached: { type: String },
  46. temporaryUrlExpiredAt: { type: Date },
  47. attachmentType: {
  48. type: String,
  49. enum: AttachmentType,
  50. required: true,
  51. },
  52. },
  53. {
  54. timestamps: { createdAt: true, updatedAt: false },
  55. },
  56. );
  57. attachmentSchema.plugin(uniqueValidator);
  58. attachmentSchema.plugin(mongoosePaginate);
  59. // virtual
  60. attachmentSchema.virtual('filePathProxied').get(function () {
  61. return `/attachment/${this._id}`;
  62. });
  63. attachmentSchema.virtual('downloadPathProxied').get(function () {
  64. return `/download/${this._id}`;
  65. });
  66. attachmentSchema.set('toObject', { virtuals: true });
  67. attachmentSchema.set('toJSON', { virtuals: true });
  68. attachmentSchema.statics.createWithoutSave = function (
  69. pageId,
  70. user,
  71. originalName: string,
  72. fileFormat: string,
  73. fileSize: number,
  74. attachmentType: AttachmentType,
  75. ) {
  76. // biome-ignore lint/complexity/noUselessThisAlias: ignore
  77. const Attachment = this;
  78. const extname = path.extname(originalName);
  79. let fileName = generateFileHash(originalName);
  80. if (extname.length > 1) {
  81. // ignore if empty or '.' only
  82. fileName = `${fileName}${extname}`;
  83. }
  84. const attachment = new Attachment();
  85. attachment.page = pageId;
  86. attachment.creator = user._id;
  87. attachment.originalName = originalName;
  88. attachment.fileName = fileName;
  89. attachment.fileFormat = fileFormat;
  90. attachment.fileSize = fileSize;
  91. attachment.attachmentType = attachmentType;
  92. return attachment;
  93. };
  94. const getValidTemporaryUrl: GetValidTemporaryUrl = function (
  95. this: IAttachmentDocument,
  96. ) {
  97. if (this.temporaryUrlExpiredAt == null) {
  98. return null;
  99. }
  100. // return null when expired url
  101. if (this.temporaryUrlExpiredAt.getTime() < new Date().getTime()) {
  102. return null;
  103. }
  104. return this.temporaryUrlCached;
  105. };
  106. attachmentSchema.methods.getValidTemporaryUrl = getValidTemporaryUrl;
  107. const cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec = function (
  108. this: IAttachmentDocument,
  109. temporaryUrl,
  110. lifetimeSec,
  111. ) {
  112. if (temporaryUrl == null) {
  113. throw new Error('url is required.');
  114. }
  115. this.temporaryUrlCached = temporaryUrl;
  116. this.temporaryUrlExpiredAt = addSeconds(new Date(), lifetimeSec);
  117. return this.save();
  118. };
  119. attachmentSchema.methods.cashTemporaryUrlByProvideSec =
  120. cashTemporaryUrlByProvideSec;
  121. export const Attachment = getOrCreateModel<
  122. IAttachmentDocument,
  123. IAttachmentModel
  124. >('Attachment', attachmentSchema);