|
@@ -5,6 +5,9 @@ import {
|
|
|
|
|
|
|
|
import loggerFactory from '~/utils/logger';
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
|
|
|
|
+import { MultipartUploader, type IMultipartUploader } from '../multipart-uploader';
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
const logger = loggerFactory('growi:services:fileUploaderAws:multipartUploader');
|
|
const logger = loggerFactory('growi:services:fileUploaderAws:multipartUploader');
|
|
|
|
|
|
|
|
enum UploadStatus {
|
|
enum UploadStatus {
|
|
@@ -14,15 +17,7 @@ enum UploadStatus {
|
|
|
ABORTED
|
|
ABORTED
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Create abstract interface IMultipartUploader in https://redmine.weseek.co.jp/issues/135775
|
|
|
|
|
-export interface IAwsMultipartUploader {
|
|
|
|
|
- initUpload(): Promise<void>;
|
|
|
|
|
- uploadPart(body: Buffer, partNumber: number): Promise<void>;
|
|
|
|
|
- completeUpload(): Promise<void>;
|
|
|
|
|
- abortUpload(): Promise<void>;
|
|
|
|
|
- uploadId: string | undefined;
|
|
|
|
|
- getUploadedFileSize(): Promise<number>;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+export type IAwsMultipartUploader = IMultipartUploader
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Class for uploading files to S3 using multipart upload.
|
|
* Class for uploading files to S3 using multipart upload.
|
|
@@ -30,32 +25,22 @@ export interface IAwsMultipartUploader {
|
|
|
* Each instance can only be used for one multipart upload, and cannot be reused once completed.
|
|
* Each instance can only be used for one multipart upload, and cannot be reused once completed.
|
|
|
* TODO: Enable creation of uploader of inturrupted uploads: https://redmine.weseek.co.jp/issues/78040
|
|
* TODO: Enable creation of uploader of inturrupted uploads: https://redmine.weseek.co.jp/issues/78040
|
|
|
*/
|
|
*/
|
|
|
-export class AwsMultipartUploader implements IAwsMultipartUploader {
|
|
|
|
|
|
|
+export class AwsMultipartUploader extends MultipartUploader implements IAwsMultipartUploader {
|
|
|
|
|
|
|
|
private bucket: string | undefined;
|
|
private bucket: string | undefined;
|
|
|
|
|
|
|
|
- private uploadKey: string;
|
|
|
|
|
-
|
|
|
|
|
- private _uploadId: string | undefined;
|
|
|
|
|
-
|
|
|
|
|
private s3Client: S3Client;
|
|
private s3Client: S3Client;
|
|
|
|
|
|
|
|
private parts: { PartNumber: number; ETag: string | undefined; }[] = [];
|
|
private parts: { PartNumber: number; ETag: string | undefined; }[] = [];
|
|
|
|
|
|
|
|
- private currentStatus: UploadStatus = UploadStatus.BEFORE_INIT;
|
|
|
|
|
-
|
|
|
|
|
- private _uploadedFileSize: number | undefined;
|
|
|
|
|
|
|
+ constructor(s3Client: S3Client, bucket: string | undefined, uploadKey: string, maxPartSize: number) {
|
|
|
|
|
+ super(uploadKey, maxPartSize);
|
|
|
|
|
|
|
|
- constructor(s3Client: S3Client, bucket: string | undefined, uploadKey: string) {
|
|
|
|
|
this.s3Client = s3Client;
|
|
this.s3Client = s3Client;
|
|
|
this.bucket = bucket;
|
|
this.bucket = bucket;
|
|
|
this.uploadKey = uploadKey;
|
|
this.uploadKey = uploadKey;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- get uploadId(): string | undefined {
|
|
|
|
|
- return this._uploadId;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
async initUpload(): Promise<void> {
|
|
async initUpload(): Promise<void> {
|
|
|
this.validateUploadStatus(UploadStatus.BEFORE_INIT);
|
|
this.validateUploadStatus(UploadStatus.BEFORE_INIT);
|
|
|
|
|
|
|
@@ -63,16 +48,20 @@ export class AwsMultipartUploader implements IAwsMultipartUploader {
|
|
|
Bucket: this.bucket,
|
|
Bucket: this.bucket,
|
|
|
Key: this.uploadKey,
|
|
Key: this.uploadKey,
|
|
|
}));
|
|
}));
|
|
|
|
|
+ if (response.UploadId == null) {
|
|
|
|
|
+ throw Error('UploadId is empty');
|
|
|
|
|
+ }
|
|
|
this._uploadId = response.UploadId;
|
|
this._uploadId = response.UploadId;
|
|
|
this.currentStatus = UploadStatus.IN_PROGRESS;
|
|
this.currentStatus = UploadStatus.IN_PROGRESS;
|
|
|
logger.info(`Multipart upload initialized. Upload key: ${this.uploadKey}`);
|
|
logger.info(`Multipart upload initialized. Upload key: ${this.uploadKey}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async uploadPart(body: Buffer, partNumber: number): Promise<void> {
|
|
|
|
|
|
|
+ async uploadPart(part: Buffer, partNumber: number): Promise<void> {
|
|
|
this.validateUploadStatus(UploadStatus.IN_PROGRESS);
|
|
this.validateUploadStatus(UploadStatus.IN_PROGRESS);
|
|
|
|
|
+ this.validatePartSize(part.length);
|
|
|
|
|
|
|
|
const uploadMetaData = await this.s3Client.send(new UploadPartCommand({
|
|
const uploadMetaData = await this.s3Client.send(new UploadPartCommand({
|
|
|
- Body: body,
|
|
|
|
|
|
|
+ Body: part,
|
|
|
Bucket: this.bucket,
|
|
Bucket: this.bucket,
|
|
|
Key: this.uploadKey,
|
|
Key: this.uploadKey,
|
|
|
PartNumber: partNumber,
|
|
PartNumber: partNumber,
|
|
@@ -83,6 +72,7 @@ export class AwsMultipartUploader implements IAwsMultipartUploader {
|
|
|
PartNumber: partNumber,
|
|
PartNumber: partNumber,
|
|
|
ETag: uploadMetaData.ETag,
|
|
ETag: uploadMetaData.ETag,
|
|
|
});
|
|
});
|
|
|
|
|
+ this._uploadedFileSize += part.length;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async completeUpload(): Promise<void> {
|
|
async completeUpload(): Promise<void> {
|
|
@@ -113,44 +103,15 @@ export class AwsMultipartUploader implements IAwsMultipartUploader {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async getUploadedFileSize(): Promise<number> {
|
|
async getUploadedFileSize(): Promise<number> {
|
|
|
- if (this._uploadedFileSize != null) return this._uploadedFileSize;
|
|
|
|
|
-
|
|
|
|
|
- this.validateUploadStatus(UploadStatus.COMPLETED);
|
|
|
|
|
- const headData = await this.s3Client.send(new HeadObjectCommand({
|
|
|
|
|
- Bucket: this.bucket,
|
|
|
|
|
- Key: this.uploadKey,
|
|
|
|
|
- }));
|
|
|
|
|
- this._uploadedFileSize = headData.ContentLength;
|
|
|
|
|
- return this._uploadedFileSize ?? 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private validateUploadStatus(desiredStatus: UploadStatus): void {
|
|
|
|
|
- if (desiredStatus === this.currentStatus) return;
|
|
|
|
|
-
|
|
|
|
|
- let errMsg: string | null = null;
|
|
|
|
|
-
|
|
|
|
|
if (this.currentStatus === UploadStatus.COMPLETED) {
|
|
if (this.currentStatus === UploadStatus.COMPLETED) {
|
|
|
- errMsg = 'Multipart upload has already been completed';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (this.currentStatus === UploadStatus.ABORTED) {
|
|
|
|
|
- errMsg = 'Multipart upload has been aborted';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // currentStatus is IN_PROGRESS or BEFORE_INIT
|
|
|
|
|
-
|
|
|
|
|
- if (this.currentStatus === UploadStatus.IN_PROGRESS && desiredStatus === UploadStatus.BEFORE_INIT) {
|
|
|
|
|
- errMsg = 'Multipart upload has already been initiated';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (this.currentStatus === UploadStatus.BEFORE_INIT && desiredStatus === UploadStatus.IN_PROGRESS) {
|
|
|
|
|
- errMsg = 'Multipart upload not initiated';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (errMsg != null) {
|
|
|
|
|
- logger.error(errMsg);
|
|
|
|
|
- throw Error(errMsg);
|
|
|
|
|
|
|
+ const headData = await this.s3Client.send(new HeadObjectCommand({
|
|
|
|
|
+ Bucket: this.bucket,
|
|
|
|
|
+ Key: this.uploadKey,
|
|
|
|
|
+ }));
|
|
|
|
|
+ if (headData.ContentLength == null) throw Error('Could not fetch uploaded file size');
|
|
|
|
|
+ this._uploadedFileSize = headData.ContentLength;
|
|
|
}
|
|
}
|
|
|
|
|
+ return this._uploadedFileSize;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|
|
}
|