Просмотр исходного кода

refactor ContentHeaders as factory method

Yuki Takei 6 месяцев назад
Родитель
Сommit
edf61b85d0

+ 3 - 3
apps/app/src/server/routes/attachment/get.ts

@@ -11,7 +11,7 @@ import type { CrowiProperties, CrowiRequest } from '~/interfaces/crowi-request';
 import { ResponseMode, type ExpressHttpHeader, type RespondOptions } from '~/server/interfaces/attachment';
 import { ResponseMode, type ExpressHttpHeader, type RespondOptions } from '~/server/interfaces/attachment';
 import {
 import {
   type FileUploader,
   type FileUploader,
-  toExpressHttpHeaders, ContentHeaders, applyHeaders,
+  toExpressHttpHeaders, applyHeaders, createContentHeaders,
 } from '~/server/service/file-uploader';
 } from '~/server/service/file-uploader';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -110,8 +110,8 @@ const respondForRedirectMode = async(res: Response, fileUploadService: FileUploa
 const respondForRelayMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
 const respondForRelayMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
   // apply content-* headers before response
   // apply content-* headers before response
   const isDownload = opts?.download ?? false;
   const isDownload = opts?.download ?? false;
-  const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
-  applyHeaders(res, contentHeaders.toExpressHttpHeaders());
+  const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
+  applyHeaders(res, contentHeaders);
 
 
   try {
   try {
     const readable = await fileUploadService.findDeliveryFile(attachment);
     const readable = await fileUploadService.findDeliveryFile(attachment);

+ 7 - 7
apps/app/src/server/service/file-uploader/aws/index.ts

@@ -28,7 +28,7 @@ import { configManager } from '../../config-manager';
 import {
 import {
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
 } from '../file-uploader';
 } from '../file-uploader';
-import { ContentHeaders } from '../utils';
+import { createContentHeaders, getContentHeaderValue } from '../utils';
 
 
 import { AwsMultipartUploader } from './multipart-uploader';
 import { AwsMultipartUploader } from './multipart-uploader';
 
 
@@ -177,7 +177,7 @@ class AwsFileUploader extends AbstractFileUploader {
     const s3 = S3Factory();
     const s3 = S3Factory();
 
 
     const filePath = getFilePathOnStorage(attachment);
     const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
+    const contentHeaders = createContentHeaders(attachment);
 
 
     await s3.send(new PutObjectCommand({
     await s3.send(new PutObjectCommand({
       Bucket: getS3Bucket(),
       Bucket: getS3Bucket(),
@@ -185,8 +185,8 @@ class AwsFileUploader extends AbstractFileUploader {
       Body: readable,
       Body: readable,
       ACL: getS3PutObjectCannedAcl(),
       ACL: getS3PutObjectCannedAcl(),
       // put type and the file name for reference information when uploading
       // put type and the file name for reference information when uploading
-      ContentType: contentHeaders.contentType?.value.toString(),
-      ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+      ContentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
+      ContentDisposition: getContentHeaderValue(contentHeaders, 'Content-Disposition'),
     }));
     }));
   }
   }
 
 
@@ -252,12 +252,12 @@ class AwsFileUploader extends AbstractFileUploader {
     // issue signed url (default: expires 120 seconds)
     // issue signed url (default: expires 120 seconds)
     // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
     // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
     const isDownload = opts?.download ?? false;
     const isDownload = opts?.download ?? false;
-    const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
+    const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
     const params: GetObjectCommandInput = {
     const params: GetObjectCommandInput = {
       Bucket: getS3Bucket(),
       Bucket: getS3Bucket(),
       Key: filePath,
       Key: filePath,
-      ResponseContentType: contentHeaders.contentType?.value.toString(),
-      ResponseContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+      ResponseContentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
+      ResponseContentDisposition: getContentHeaderValue(contentHeaders, 'Content-Disposition'),
     };
     };
     const signedUrl = await getSignedUrl(s3, new GetObjectCommand(params), {
     const signedUrl = await getSignedUrl(s3, new GetObjectCommand(params), {
       expiresIn: lifetimeSecForTemporaryUrl,
       expiresIn: lifetimeSecForTemporaryUrl,

+ 7 - 7
apps/app/src/server/service/file-uploader/azure.ts

@@ -29,7 +29,7 @@ import { configManager } from '../config-manager';
 import {
 import {
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
 } from './file-uploader';
 } from './file-uploader';
-import { ContentHeaders } from './utils';
+import { createContentHeaders, getContentHeaderValue } from './utils';
 
 
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 
 
@@ -132,13 +132,13 @@ class AzureFileUploader extends AbstractFileUploader {
     const filePath = getFilePathOnStorage(attachment);
     const filePath = getFilePathOnStorage(attachment);
     const containerClient = await getContainerClient();
     const containerClient = await getContainerClient();
     const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
     const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
-    const contentHeaders = new ContentHeaders(attachment);
+    const contentHeaders = createContentHeaders(attachment);
 
 
     await blockBlobClient.uploadStream(readable, undefined, undefined, {
     await blockBlobClient.uploadStream(readable, undefined, undefined, {
       blobHTTPHeaders: {
       blobHTTPHeaders: {
         // put type and the file name for reference information when uploading
         // put type and the file name for reference information when uploading
-        blobContentType: contentHeaders.contentType?.value.toString(),
-        blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+        blobContentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
+        blobContentDisposition: getContentHeaderValue(contentHeaders, 'Content-Disposition'),
       },
       },
     });
     });
   }
   }
@@ -210,7 +210,7 @@ class AzureFileUploader extends AbstractFileUploader {
       const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
       const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
 
 
       const isDownload = opts?.download ?? false;
       const isDownload = opts?.download ?? false;
-      const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
+      const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
 
 
       // https://github.com/Azure/azure-sdk-for-js/blob/d4d55f73/sdk/storage/storage-blob/src/ContainerSASPermissions.ts#L24
       // https://github.com/Azure/azure-sdk-for-js/blob/d4d55f73/sdk/storage/storage-blob/src/ContainerSASPermissions.ts#L24
       // r:read, a:add, c:create, w:write, d:delete, l:list
       // r:read, a:add, c:create, w:write, d:delete, l:list
@@ -221,8 +221,8 @@ class AzureFileUploader extends AbstractFileUploader {
         protocol: SASProtocol.HttpsAndHttp,
         protocol: SASProtocol.HttpsAndHttp,
         startsOn,
         startsOn,
         expiresOn,
         expiresOn,
-        contentType: contentHeaders.contentType?.value.toString(),
-        contentDisposition: contentHeaders.contentDisposition?.value.toString(),
+        contentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
+        contentDisposition: getContentHeaderValue(contentHeaders, 'Content-Disposition'),
       };
       };
 
 
       return generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();
       return generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();

+ 6 - 6
apps/app/src/server/service/file-uploader/gcs/index.ts

@@ -17,7 +17,7 @@ import { configManager } from '../../config-manager';
 import {
 import {
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
 } from '../file-uploader';
 } from '../file-uploader';
-import { ContentHeaders } from '../utils';
+import { createContentHeaders, getContentHeaderValue } from '../utils';
 
 
 import { GcsMultipartUploader } from './multipart-uploader';
 import { GcsMultipartUploader } from './multipart-uploader';
 
 
@@ -131,12 +131,12 @@ class GcsFileUploader extends AbstractFileUploader {
     const gcs = getGcsInstance();
     const gcs = getGcsInstance();
     const myBucket = gcs.bucket(getGcsBucket());
     const myBucket = gcs.bucket(getGcsBucket());
     const filePath = getFilePathOnStorage(attachment);
     const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
+    const contentHeaders = createContentHeaders(attachment);
 
 
     const file = myBucket.file(filePath);
     const file = myBucket.file(filePath);
     const writeStream = file.createWriteStream({
     const writeStream = file.createWriteStream({
       // put type and the file name for reference information when uploading
       // put type and the file name for reference information when uploading
-      contentType: contentHeaders.contentType?.value.toString(),
+      contentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
     });
     });
 
 
     try {
     try {
@@ -217,12 +217,12 @@ class GcsFileUploader extends AbstractFileUploader {
     // issue signed url (default: expires 120 seconds)
     // issue signed url (default: expires 120 seconds)
     // https://cloud.google.com/storage/docs/access-control/signed-urls
     // https://cloud.google.com/storage/docs/access-control/signed-urls
     const isDownload = opts?.download ?? false;
     const isDownload = opts?.download ?? false;
-    const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
+    const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
     const [signedUrl] = await file.getSignedUrl({
     const [signedUrl] = await file.getSignedUrl({
       action: 'read',
       action: 'read',
       expires: Date.now() + lifetimeSecForTemporaryUrl * 1000,
       expires: Date.now() + lifetimeSecForTemporaryUrl * 1000,
-      responseType: contentHeaders.contentType?.value.toString(),
-      responseDisposition: contentHeaders.contentDisposition?.value.toString(),
+      responseType: getContentHeaderValue(contentHeaders, 'Content-Type'),
+      responseDisposition: getContentHeaderValue(contentHeaders, 'Content-Disposition'),
     });
     });
 
 
     return {
     return {

+ 3 - 3
apps/app/src/server/service/file-uploader/gridfs.ts

@@ -12,7 +12,7 @@ import loggerFactory from '~/utils/logger';
 import { configManager } from '../config-manager';
 import { configManager } from '../config-manager';
 
 
 import { AbstractFileUploader, type TemporaryUrl, type SaveFileParam } from './file-uploader';
 import { AbstractFileUploader, type TemporaryUrl, type SaveFileParam } from './file-uploader';
-import { ContentHeaders } from './utils';
+import { createContentHeaders, getContentHeaderValue } from './utils';
 
 
 const logger = loggerFactory('growi:service:fileUploaderGridfs');
 const logger = loggerFactory('growi:service:fileUploaderGridfs');
 
 
@@ -65,13 +65,13 @@ class GridfsFileUploader extends AbstractFileUploader {
   override async uploadAttachment(readable: Readable, attachment: IAttachmentDocument): Promise<void> {
   override async uploadAttachment(readable: Readable, attachment: IAttachmentDocument): Promise<void> {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
 
-    const contentHeaders = new ContentHeaders(attachment);
+    const contentHeaders = createContentHeaders(attachment);
 
 
     return AttachmentFile.promisifiedWrite(
     return AttachmentFile.promisifiedWrite(
       {
       {
         // put type and the file name for reference information when uploading
         // put type and the file name for reference information when uploading
         filename: attachment.fileName,
         filename: attachment.fileName,
-        contentType: contentHeaders.contentType?.value.toString(),
+        contentType: getContentHeaderValue(contentHeaders, 'Content-Type'),
       },
       },
       readable,
       readable,
     );
     );

+ 3 - 3
apps/app/src/server/service/file-uploader/local.ts

@@ -15,7 +15,7 @@ import {
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
   AbstractFileUploader, type TemporaryUrl, type SaveFileParam,
 } from './file-uploader';
 } from './file-uploader';
 import {
 import {
-  ContentHeaders, applyHeaders,
+  applyHeaders, createContentHeaders, toExpressHttpHeaders,
 } from './utils';
 } from './utils';
 
 
 
 
@@ -229,9 +229,9 @@ module.exports = function(crowi: Crowi) {
     const internalPath = urljoin(internalPathRoot, relativePath);
     const internalPath = urljoin(internalPathRoot, relativePath);
 
 
     const isDownload = opts?.download ?? false;
     const isDownload = opts?.download ?? false;
-    const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
+    const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
     applyHeaders(res, [
     applyHeaders(res, [
-      ...contentHeaders.toExpressHttpHeaders(),
+      ...toExpressHttpHeaders(contentHeaders),
       { field: 'X-Accel-Redirect', value: internalPath },
       { field: 'X-Accel-Redirect', value: internalPath },
       { field: 'X-Sendfile', value: storagePath },
       { field: 'X-Sendfile', value: storagePath },
     ]);
     ]);

+ 47 - 47
apps/app/src/server/service/file-uploader/utils/headers.ts

@@ -1,68 +1,68 @@
 import type { Response } from 'express';
 import type { Response } from 'express';
 
 
-import type { ExpressHttpHeader, IContentHeaders } from '~/server/interfaces/attachment';
+import type { ExpressHttpHeader } from '~/server/interfaces/attachment';
 import type { IAttachmentDocument } from '~/server/models/attachment';
 import type { IAttachmentDocument } from '~/server/models/attachment';
 
 
+type ContentHeaderField = 'Content-Type' | 'Content-Security-Policy' | 'Content-Disposition' | 'Content-Length';
+type ContentHeader = ExpressHttpHeader<ContentHeaderField>;
 
 
-export class ContentHeaders implements IContentHeaders {
-
-  contentType?: ExpressHttpHeader<'Content-Type'>;
+/**
+ * Factory function to generate content headers.
+ * This approach avoids creating a class instance for each call, improving memory efficiency.
+ */
+export const createContentHeaders = (attachment: IAttachmentDocument, opts?: { inline?: boolean }): ContentHeader[] => {
+  const headers: ContentHeader[] = [];
 
 
-  contentLength?: ExpressHttpHeader<'Content-Length'>;
+  // Content-Type
+  headers.push({
+    field: 'Content-Type',
+    value: attachment.fileFormat,
+  });
 
 
-  contentSecurityPolicy?: ExpressHttpHeader<'Content-Security-Policy'>;
+  // Content-Security-Policy
+  headers.push({
+    field: 'Content-Security-Policy',
+    // eslint-disable-next-line max-len
+    value: "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
+  });
 
 
-  contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
+  // Content-Disposition
+  headers.push({
+    field: 'Content-Disposition',
+    value: `${opts?.inline ? 'inline' : 'attachment'};filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
+  });
 
 
-  constructor(attachment: IAttachmentDocument, opts?: {
-    inline?: boolean,
-  }) {
+  // Content-Length
+  if (attachment.fileSize != null) {
+    headers.push({
+      field: 'Content-Length',
+      value: attachment.fileSize.toString(),
+    });
+  }
 
 
-    this.contentType = {
-      field: 'Content-Type',
-      value: attachment.fileFormat,
-    };
-    this.contentSecurityPolicy = {
-      field: 'Content-Security-Policy',
-      // eslint-disable-next-line max-len
-      value: "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
-    };
-    this.contentDisposition = {
-      field: 'Content-Disposition',
-      value: `${opts?.inline ? 'inline' : 'attachment'};filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
-    };
+  return headers;
+};
 
 
-    if (attachment.fileSize) {
-      this.contentLength = {
-        field: 'Content-Length',
-        value: attachment.fileSize.toString(),
-      };
-    }
-  }
+export const getContentHeaderValue = (contentHeaders: ContentHeader[], field: ContentHeaderField): string | undefined => {
+  const header = contentHeaders.find(h => h.field === field);
+  return header?.value.toString();
+};
 
 
-  /**
-   * Convert to ExpressHttpHeader[]
-   */
-  toExpressHttpHeaders(): ExpressHttpHeader[] {
-    return [
-      this.contentType,
-      this.contentLength,
-      this.contentSecurityPolicy,
-      this.contentDisposition,
-    ]
+/**
+ * Convert to ExpressHttpHeader[]
+ */
+export function toExpressHttpHeaders(records: Record<string, string | string[]>): ExpressHttpHeader[];
+export function toExpressHttpHeaders(contentHeaders: ContentHeader[]): ExpressHttpHeader[];
+export function toExpressHttpHeaders(arg: Record<string, string | string[]> | ContentHeader[]): ExpressHttpHeader[] {
+  if (Array.isArray(arg)) {
+    return arg
       // exclude undefined
       // exclude undefined
       .filter((member): member is NonNullable<typeof member> => member != null);
       .filter((member): member is NonNullable<typeof member> => member != null);
   }
   }
 
 
+  return Object.entries(arg).map(([field, value]) => { return { field, value } });
 }
 }
 
 
-/**
- * Convert Record to ExpressHttpHeader[]
- */
-export const toExpressHttpHeaders = (records: Record<string, string | string[]>): ExpressHttpHeader[] => {
-  return Object.entries(records).map(([field, value]) => { return { field, value } });
-};
-
 export const applyHeaders = (res: Response, headers: ExpressHttpHeader[]): void => {
 export const applyHeaders = (res: Response, headers: ExpressHttpHeader[]): void => {
   headers.forEach((header) => {
   headers.forEach((header) => {
     res.header(header.field, header.value);
     res.header(header.field, header.value);