chore(medusa-file-s3): Add cache-control option + update to sdk v3 (#4884)

* add cache-control option, fix delete function, update to sdk v3

* Create cool-pears-trade.md

* reformat throw line, remove dev comment

* remove console.log debugging statement

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Lacey PeveyandOli Juhl authored and GitHub committed 2023-08-29 19:25:17 +02:00
1 parent 33ffdaf42d
commit 046b0dcfa5
4 files changed
+1314 -51

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"medusa-file-s3": patch
---
Chore(medusa-file-s3): Add cache-control option, fix delete function, update to sdk v3
+3 -1
View File
@@ -32,7 +32,9 @@
"@medusajs/medusa": "^1.12.0" "@medusajs/medusa": "^1.12.0"
}, },
"dependencies": { "dependencies": {
"aws-sdk": "^2.983.0", "@aws-sdk/client-s3": "^3.400.0",
"@aws-sdk/lib-storage": "^3.400.0",
"@aws-sdk/s3-request-presigner": "^3.400.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.2.0", "medusa-core-utils": "^1.2.0",
+57 -45
View File
@@ -1,5 +1,13 @@
import fs from "fs" import fs from "fs"
import aws from "aws-sdk" import type { S3ClientConfigType, PutObjectCommandInput, GetObjectCommandOutput } from "@aws-sdk/client-s3"
import { Upload } from "@aws-sdk/lib-storage"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
GetObjectCommand
} from "@aws-sdk/client-s3"
import { parse } from "path" import { parse } from "path"
import { AbstractFileService, IFileService } from "@medusajs/medusa" import { AbstractFileService, IFileService } from "@medusajs/medusa"
import { import {
@@ -7,10 +15,9 @@ import {
FileServiceUploadResult, FileServiceUploadResult,
GetUploadedFileType, GetUploadedFileType,
UploadStreamDescriptorType, UploadStreamDescriptorType,
Logger
} from "@medusajs/types" } from "@medusajs/types"
import stream from "stream" import stream from "stream"
import { PutObjectRequest } from "aws-sdk/clients/s3"
import { ClientConfiguration } from "aws-sdk/clients/s3"
class S3Service extends AbstractFileService implements IFileService { class S3Service extends AbstractFileService implements IFileService {
protected bucket_: string protected bucket_: string
@@ -18,11 +25,13 @@ class S3Service extends AbstractFileService implements IFileService {
protected accessKeyId_: string protected accessKeyId_: string
protected secretAccessKey_: string protected secretAccessKey_: string
protected region_: string protected region_: string
protected endpoint_: string
protected awsConfigObject_: any protected awsConfigObject_: any
protected downloadFileDuration_: string protected downloadFileDuration_: number
protected cacheControl_: string
protected logger_: Logger
protected client_: S3Client
constructor({}, options) { constructor({ logger }, options) {
super({}, options) super({}, options)
this.bucket_ = options.bucket this.bucket_ = options.bucket
@@ -30,22 +39,26 @@ class S3Service extends AbstractFileService implements IFileService {
this.accessKeyId_ = options.access_key_id this.accessKeyId_ = options.access_key_id
this.secretAccessKey_ = options.secret_access_key this.secretAccessKey_ = options.secret_access_key
this.region_ = options.region this.region_ = options.region
this.endpoint_ = options.endpoint
this.downloadFileDuration_ = options.download_file_duration this.downloadFileDuration_ = options.download_file_duration
this.awsConfigObject_ = options.aws_config_object ?? {} this.awsConfigObject_ = options.aws_config_object ?? {}
this.cacheControl_ = options.cache_control ?? "max-age=31536000"
this.logger_ = logger
this.client_ = this.getClient()
} }
protected getClient(overwriteConfig: Partial<ClientConfiguration> = {}) { protected getClient(overwriteConfig: Partial<S3ClientConfigType> = {}) {
const config: ClientConfiguration = { const config: S3ClientConfigType = {
credentials: {
accessKeyId: this.accessKeyId_, accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_, secretAccessKey: this.secretAccessKey_,
},
region: this.region_, region: this.region_,
endpoint: this.endpoint_,
...this.awsConfigObject_, ...this.awsConfigObject_,
signatureVersion: 'v4',
...overwriteConfig, ...overwriteConfig,
} }
return new aws.S3(config) return new S3Client(config)
} }
async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> { async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> {
@@ -63,55 +76,52 @@ class S3Service extends AbstractFileService implements IFileService {
acl: undefined, acl: undefined,
} }
) { ) {
const client = this.getClient()
const parsedFilename = parse(file.originalname) const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const params = { const command = new PutObjectCommand({
ACL: options.acl ?? (options.isProtected ? "private" : "public-read"), ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
Bucket: this.bucket_, Bucket: this.bucket_,
Body: fs.createReadStream(file.path), Body: fs.createReadStream(file.path),
Key: fileKey, Key: fileKey,
ContentType: file.mimetype, ContentType: file.mimetype,
} CacheControl: this.cacheControl_
})
const result = await client.upload(params).promise()
try {
await this.client_.send(command)
return { return {
url: result.Location, url: `${this.s3Url_}/${fileKey}`,
key: result.Key, key: fileKey,
}
} catch (e) {
this.logger_.error(e)
throw e
} }
} }
async delete(file: DeleteFileType): Promise<void> { async delete(file: DeleteFileType): Promise<void> {
const client = this.getClient() const command = new DeleteObjectCommand({
const params = {
Bucket: this.bucket_, Bucket: this.bucket_,
Key: `${file}`, Key: `${file.file_key}`,
} })
return new Promise((resolve, reject) => { try {
client.deleteObject(params, (err, data) => { await this.client_.send(command)
if (err) { } catch (e) {
reject(err) this.logger_.error(e)
return
} }
resolve()
})
})
} }
async getUploadStreamDescriptor(fileData: UploadStreamDescriptorType) { async getUploadStreamDescriptor(fileData: UploadStreamDescriptorType) {
const client = this.getClient()
const pass = new stream.PassThrough() const pass = new stream.PassThrough()
const isPrivate = fileData.isPrivate ?? true // default to private const isPrivate = fileData.isPrivate ?? true // default to private
const fileKey = `${fileData.name}.${fileData.ext}` const fileKey = `${fileData.name}.${fileData.ext}`
const params: PutObjectRequest = { const params: PutObjectCommandInput = {
ACL: isPrivate ? "private" : "public-read", ACL: isPrivate ? "private" : "public-read",
Bucket: this.bucket_, Bucket: this.bucket_,
Body: pass, Body: pass,
@@ -119,9 +129,14 @@ class S3Service extends AbstractFileService implements IFileService {
ContentType: fileData.contentType as string, ContentType: fileData.contentType as string,
} }
const uploadJob = new Upload({
client: this.client_,
params
})
return { return {
writeStream: pass, writeStream: pass,
promise: client.upload(params).promise(), promise: uploadJob.done(),
url: `${this.s3Url_}/${fileKey}`, url: `${this.s3Url_}/${fileKey}`,
fileKey, fileKey,
} }
@@ -130,28 +145,25 @@ class S3Service extends AbstractFileService implements IFileService {
async getDownloadStream( async getDownloadStream(
fileData: GetUploadedFileType fileData: GetUploadedFileType
): Promise<NodeJS.ReadableStream> { ): Promise<NodeJS.ReadableStream> {
const client = this.getClient() const command = new GetObjectCommand({
const params = {
Bucket: this.bucket_, Bucket: this.bucket_,
Key: `${fileData.fileKey}`, Key: `${fileData.fileKey}`,
} })
return await client.getObject(params).createReadStream() const response: GetObjectCommandOutput = await this.client_.send(command)
return response.Body as NodeJS.ReadableStream
} }
async getPresignedDownloadUrl( async getPresignedDownloadUrl(
fileData: GetUploadedFileType fileData: GetUploadedFileType
): Promise<string> { ): Promise<string> {
const client = this.getClient({ signatureVersion: "v4" }) const command = new GetObjectCommand({
const params = {
Bucket: this.bucket_, Bucket: this.bucket_,
Key: `${fileData.fileKey}`, Key: `${fileData.fileKey}`,
Expires: this.downloadFileDuration_, })
}
return await client.getSignedUrlPromise("getObject", params) return await getSignedUrl(this.client_, command, { expiresIn: this.downloadFileDuration_ })
} }
} }
+1248 -5
View File
File diff suppressed because it is too large. Load diff