Selaa lähdekoodia

refactor FileUploader

Yuki Takei 1 vuosi sitten
vanhempi
sitoutus
9d4794d63c

+ 28 - 23
apps/app/src/server/service/file-uploader/aws.ts

@@ -1,3 +1,5 @@
+import type { ReadStream } from 'fs';
+
 import type { GetObjectCommandInput, HeadObjectCommandInput } from '@aws-sdk/client-s3';
 import {
   S3Client,
@@ -142,6 +144,32 @@ class AwsFileUploader extends AbstractFileUploader {
       : ResponseMode.REDIRECT;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('AWS is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const s3 = S3Factory();
+
+    const filePath = getFilePathOnStorage(attachment);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await s3.send(new PutObjectCommand({
+      Bucket: getS3Bucket(),
+      Key: filePath,
+      Body: readStream,
+      ACL: getS3PutObjectCannedAcl(),
+      // put type and the file name for reference information when uploading
+      ContentType: contentHeaders.contentType?.value.toString(),
+      ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+    }));
+  }
+
   /**
    * @inheritdoc
    */
@@ -280,29 +308,6 @@ module.exports = (crowi) => {
     return s3.send(new DeleteObjectCommand(params));
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('AWS is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const s3 = S3Factory();
-
-    const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return s3.send(new PutObjectCommand({
-      Bucket: getS3Bucket(),
-      Key: filePath,
-      Body: fileStream,
-      ACL: getS3PutObjectCannedAcl(),
-      // put type and the file name for reference information when uploading
-      ContentType: contentHeaders.contentType?.value.toString(),
-      ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
-    }));
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const s3 = S3Factory();
 

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

@@ -1,11 +1,16 @@
-import { ClientSecretCredential, TokenCredential } from '@azure/identity';
-import {
-  generateBlobSASQueryParameters,
-  BlobServiceClient,
+import type { ReadStream } from 'fs';
+
+import type { TokenCredential } from '@azure/identity';
+import { ClientSecretCredential } from '@azure/identity';
+import type {
   BlobClient,
   BlockBlobClient,
   BlobDeleteOptions,
   ContainerClient,
+} from '@azure/storage-blob';
+import {
+  generateBlobSASQueryParameters,
+  BlobServiceClient,
   ContainerSASPermissions,
   SASProtocol,
   type BlobDeleteIfExistsResponse,
@@ -94,6 +99,29 @@ class AzureFileUploader extends AbstractFileUploader {
     throw new Error('Method not implemented.');
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('Azure is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+    const filePath = getFilePathOnStorage(attachment);
+    const containerClient = await getContainerClient();
+    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await blockBlobClient.uploadStream(readStream, undefined, undefined, {
+      blobHTTPHeaders: {
+        // put type and the file name for reference information when uploading
+        blobContentType: contentHeaders.contentType?.value.toString(),
+        blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+      },
+    });
+  }
+
   /**
    * @inheritdoc
    */
@@ -218,26 +246,6 @@ module.exports = (crowi) => {
     }
   };
 
-  (lib as any).uploadAttachment = async function(readStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('Azure is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-    const filePath = getFilePathOnStorage(attachment);
-    const containerClient = await getContainerClient();
-    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return blockBlobClient.uploadStream(readStream, undefined, undefined, {
-      blobHTTPHeaders: {
-        // put type and the file name for reference information when uploading
-        blobContentType: contentHeaders.contentType?.value.toString(),
-        blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
-      },
-    });
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const containerClient = await getContainerClient();
     const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);

+ 4 - 0
apps/app/src/server/service/file-uploader/file-uploader.ts

@@ -1,4 +1,5 @@
 import { randomUUID } from 'crypto';
+import type { Readable } from 'stream';
 
 import type { Response } from 'express';
 
@@ -36,6 +37,7 @@ export interface FileUploader {
   getTotalFileSize(): Promise<number>,
   doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<ICheckLimitResult>,
   determineResponseMode(): ResponseMode,
+  uploadAttachment(readStream: Readable, attachment: IAttachmentDocument): Promise<void>,
   respond(res: Response, attachment: IAttachmentDocument, opts?: RespondOptions): void,
   findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream>,
   generateTemporaryUrl(attachment: IAttachmentDocument, opts?: RespondOptions): Promise<TemporaryUrl>,
@@ -151,6 +153,8 @@ export abstract class AbstractFileUploader implements FileUploader {
     return ResponseMode.RELAY;
   }
 
+ abstract uploadAttachment(readStream: Readable, attachment: IAttachmentDocument): Promise<void>;
+
   /**
    * Respond to the HTTP request.
    */

+ 24 - 19
apps/app/src/server/service/file-uploader/gcs.ts

@@ -1,3 +1,5 @@
+import type { ReadStream } from 'fs';
+
 import { Storage } from '@google-cloud/storage';
 import urljoin from 'url-join';
 
@@ -94,6 +96,28 @@ class GcsFileUploader extends AbstractFileUploader {
       : ResponseMode.REDIRECT;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('GCS is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const gcs = getGcsInstance();
+    const myBucket = gcs.bucket(getGcsBucket());
+    const filePath = getFilePathOnStorage(attachment);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await myBucket.upload(readStream.path.toString(), {
+      destination: filePath,
+      // put type and the file name for reference information when uploading
+      contentType: contentHeaders.contentType?.value.toString(),
+    });
+  }
+
   /**
    * @inheritdoc
    */
@@ -201,25 +225,6 @@ module.exports = function(crowi: Crowi) {
     });
   };
 
-  (lib as any).uploadAttachment = function(fileStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('GCS is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const gcs = getGcsInstance();
-    const myBucket = gcs.bucket(getGcsBucket());
-    const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return myBucket.upload(fileStream.path, {
-      destination: filePath,
-      // put type and the file name for reference information when uploading
-      contentType: contentHeaders.contentType?.value.toString(),
-    });
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const gcs = getGcsInstance();
     const myBucket = gcs.bucket(getGcsBucket());

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

@@ -1,3 +1,4 @@
+import type { ReadStream } from 'fs';
 import { Readable } from 'stream';
 import util from 'util';
 
@@ -16,6 +17,17 @@ import { ContentHeaders } from './utils';
 const logger = loggerFactory('growi:service:fileUploaderGridfs');
 
 
+const COLLECTION_NAME = 'attachmentFiles';
+const CHUNK_COLLECTION_NAME = `${COLLECTION_NAME}.chunks`;
+
+// instantiate mongoose-gridfs
+const AttachmentFile = createModel({
+  modelName: COLLECTION_NAME,
+  bucketName: COLLECTION_NAME,
+  connection: mongoose.connection,
+});
+
+
 // TODO: rewrite this module to be a type-safe implementation
 class GridfsFileUploader extends AbstractFileUploader {
 
@@ -47,6 +59,24 @@ class GridfsFileUploader extends AbstractFileUploader {
     throw new Error('Method not implemented.');
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const contentHeaders = new ContentHeaders(attachment);
+
+    return AttachmentFile.promisifiedWrite(
+      {
+        // put type and the file name for reference information when uploading
+        filename: attachment.fileName,
+        contentType: contentHeaders.contentType?.value.toString(),
+      },
+      readStream,
+    );
+  }
+
   /**
    * @inheritdoc
    */
@@ -73,15 +103,6 @@ class GridfsFileUploader extends AbstractFileUploader {
 
 module.exports = function(crowi) {
   const lib = new GridfsFileUploader(crowi);
-  const COLLECTION_NAME = 'attachmentFiles';
-  const CHUNK_COLLECTION_NAME = `${COLLECTION_NAME}.chunks`;
-
-  // instantiate mongoose-gridfs
-  const AttachmentFile = createModel({
-    modelName: COLLECTION_NAME,
-    bucketName: COLLECTION_NAME,
-    connection: mongoose.connection,
-  });
 
   // get Collection instance of chunk
   const chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
@@ -150,21 +171,6 @@ module.exports = function(crowi) {
     return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return AttachmentFile.promisifiedWrite(
-      {
-        // put type and the file name for reference information when uploading
-        filename: attachment.fileName,
-        contentType: contentHeaders.contentType?.value.toString(),
-      },
-      fileStream,
-    );
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const readable = new Readable();
     readable.push(data);

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

@@ -1,3 +1,4 @@
+import type { ReadStream } from 'fs';
 import { Readable } from 'stream';
 
 import type { Response } from 'express';
@@ -71,6 +72,13 @@ class LocalFileUploader extends AbstractFileUploader {
       : ResponseMode.RELAY;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    throw new Error('Method not implemented.');
+  }
+
   /**
    * @inheritdoc
    */
@@ -146,7 +154,7 @@ module.exports = function(crowi) {
     return fs.unlinkSync(filePath);
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
+  lib.uploadAttachment = async function(fileStream, attachment) {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
     const filePath = getFilePathOnStorage(attachment);