feat(medusa-file-s3,medusa-file-minio): Upgrade to TypeScript (#3740)
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
"name": "medusa-file-minio",
|
||||
"version": "1.1.6",
|
||||
"description": "MinIO server file connector for Medusa",
|
||||
"main": "index.js",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
@@ -11,32 +14,22 @@
|
||||
"author": "Edin Skeja",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/node": "^7.16.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.0",
|
||||
"@babel/plugin-transform-instanceof": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/register": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"client-sessions": "^0.8.0",
|
||||
"@medusajs/medusa": "1.8.0-rc.6",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"medusa-interfaces": "^1.3.7"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'",
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"watch": "babel -w src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'",
|
||||
"test": "jest --passWithNoTests src"
|
||||
"test": "jest --passWithNoTests src",
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.3.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.16.0",
|
||||
"aws-sdk": "^2.1043.0",
|
||||
"aws-sdk": "^2.983.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^1.2.0",
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import stream from "stream"
|
||||
import aws from "aws-sdk"
|
||||
import { parse } from "path"
|
||||
import fs from "fs"
|
||||
import { AbstractFileService } from "@medusajs/medusa"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
class MinioService extends AbstractFileService {
|
||||
constructor({}, options) {
|
||||
super({}, options)
|
||||
|
||||
this.bucket_ = options.bucket
|
||||
this.accessKeyId_ = options.access_key_id
|
||||
this.secretAccessKey_ = options.secret_access_key
|
||||
this.private_bucket_ = options.private_bucket
|
||||
this.private_access_key_id_ =
|
||||
options.private_access_key_id ?? this.accessKeyId_
|
||||
this.private_secret_access_key_ =
|
||||
options.private_secret_access_key ?? this.secretAccessKey_
|
||||
this.endpoint_ = options.endpoint
|
||||
this.s3ForcePathStyle_ = true
|
||||
this.signatureVersion_ = "v4"
|
||||
this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds
|
||||
}
|
||||
|
||||
upload(file) {
|
||||
this.updateAwsConfig_()
|
||||
|
||||
return this.uploadFile(file)
|
||||
}
|
||||
|
||||
uploadProtected(file) {
|
||||
this.validatePrivateBucketConfiguration_(true)
|
||||
this.updateAwsConfig_(true)
|
||||
|
||||
return this.uploadFile(file, { isProtected: true })
|
||||
}
|
||||
|
||||
uploadFile(file, options = { isProtected: false }) {
|
||||
const parsedFilename = parse(file.originalname)
|
||||
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
|
||||
|
||||
const s3 = new aws.S3()
|
||||
const params = {
|
||||
ACL: options.isProtected ? "private" : "public-read",
|
||||
Bucket: options.isProtected ? this.private_bucket_ : this.bucket_,
|
||||
Body: fs.createReadStream(file.path),
|
||||
Key: fileKey,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.upload(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
resolve({ url: data.Location, key: data.Key })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async delete(file) {
|
||||
this.updateAwsConfig_()
|
||||
|
||||
const s3 = new aws.S3()
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${file.fileKey}`,
|
||||
}
|
||||
|
||||
return await Promise.all([
|
||||
new Promise((resolve, reject) =>
|
||||
s3.deleteObject({ ...params, Bucket: this.bucket_ }, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
),
|
||||
new Promise((resolve, reject) =>
|
||||
s3.deleteObject(
|
||||
{ ...params, Bucket: this.private_bucket_ },
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
}
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
async getUploadStreamDescriptor({ usePrivateBucket = true, ...fileData }) {
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
this.updateAwsConfig_(usePrivateBucket)
|
||||
|
||||
const pass = new stream.PassThrough()
|
||||
|
||||
const fileKey = `${fileData.name}.${fileData.ext}`
|
||||
const params = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Body: pass,
|
||||
Key: fileKey,
|
||||
}
|
||||
|
||||
const s3 = new aws.S3()
|
||||
return {
|
||||
writeStream: pass,
|
||||
promise: s3.upload(params).promise(),
|
||||
url: `${this.spacesUrl_}/${fileKey}`,
|
||||
fileKey,
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadStream({ usePrivateBucket = true, ...fileData }) {
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
this.updateAwsConfig_(usePrivateBucket)
|
||||
|
||||
const s3 = new aws.S3()
|
||||
|
||||
const params = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
}
|
||||
|
||||
return s3.getObject(params).createReadStream()
|
||||
}
|
||||
|
||||
async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) {
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
this.updateAwsConfig_(usePrivateBucket, {
|
||||
signatureVersion: "v4",
|
||||
})
|
||||
|
||||
const s3 = new aws.S3()
|
||||
|
||||
const params = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
Expires: this.downloadUrlDuration,
|
||||
}
|
||||
|
||||
return await s3.getSignedUrlPromise("getObject", params)
|
||||
}
|
||||
|
||||
validatePrivateBucketConfiguration_(usePrivateBucket) {
|
||||
if (
|
||||
usePrivateBucket &&
|
||||
(!this.private_access_key_id_ || !this.private_bucket_)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Private bucket is not configured"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateAwsConfig_(usePrivateBucket = false, additionalConfiguration = {}) {
|
||||
aws.config.setPromisesDependency(null)
|
||||
aws.config.update(
|
||||
{
|
||||
accessKeyId: usePrivateBucket
|
||||
? this.private_access_key_id_
|
||||
: this.accessKeyId_,
|
||||
secretAccessKey: usePrivateBucket
|
||||
? this.private_secret_access_key_
|
||||
: this.secretAccessKey_,
|
||||
endpoint: this.endpoint_,
|
||||
s3ForcePathStyle: this.s3ForcePathStyle_,
|
||||
signatureVersion: this.signatureVersion_,
|
||||
...additionalConfiguration,
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MinioService
|
||||
@@ -0,0 +1,212 @@
|
||||
import stream from "stream"
|
||||
import aws from "aws-sdk"
|
||||
import { parse } from "path"
|
||||
import fs from "fs"
|
||||
import {
|
||||
AbstractFileService,
|
||||
DeleteFileType,
|
||||
FileServiceUploadResult,
|
||||
GetUploadedFileType,
|
||||
IFileService,
|
||||
UploadStreamDescriptorType,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { ClientConfiguration, PutObjectRequest } from "aws-sdk/clients/s3"
|
||||
|
||||
class MinioService extends AbstractFileService implements IFileService {
|
||||
protected bucket_: string
|
||||
protected accessKeyId_: string
|
||||
protected secretAccessKey_: string
|
||||
protected private_bucket_: string
|
||||
protected private_access_key_id_: string
|
||||
protected private_secret_access_key_: string
|
||||
protected endpoint_: string
|
||||
protected s3ForcePathStyle_: boolean
|
||||
protected signatureVersion_: string
|
||||
protected downloadUrlDuration: string | number
|
||||
|
||||
constructor({}, options) {
|
||||
super({}, options)
|
||||
|
||||
this.bucket_ = options.bucket
|
||||
this.accessKeyId_ = options.access_key_id
|
||||
this.secretAccessKey_ = options.secret_access_key
|
||||
this.private_bucket_ = options.private_bucket
|
||||
this.private_access_key_id_ =
|
||||
options.private_access_key_id ?? this.accessKeyId_
|
||||
this.private_secret_access_key_ =
|
||||
options.private_secret_access_key ?? this.secretAccessKey_
|
||||
this.endpoint_ = options.endpoint
|
||||
this.s3ForcePathStyle_ = true
|
||||
this.signatureVersion_ = "v4"
|
||||
this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds
|
||||
}
|
||||
|
||||
protected buildUrl(bucket: string, key: string) {
|
||||
return `${this.endpoint_}/${bucket}/${key}`
|
||||
}
|
||||
|
||||
async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> {
|
||||
return await this.uploadFile(file)
|
||||
}
|
||||
|
||||
async uploadProtected(
|
||||
file: Express.Multer.File
|
||||
): Promise<FileServiceUploadResult> {
|
||||
this.validatePrivateBucketConfiguration_(true)
|
||||
|
||||
return await this.uploadFile(file, { isProtected: true })
|
||||
}
|
||||
|
||||
protected async uploadFile(
|
||||
file: Express.Multer.File,
|
||||
options: { isProtected: boolean } = { isProtected: false }
|
||||
) {
|
||||
const parsedFilename = parse(file.originalname)
|
||||
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
|
||||
|
||||
const client = this.getClient(options.isProtected)
|
||||
|
||||
const params = {
|
||||
ACL: options.isProtected ? "private" : "public-read",
|
||||
Bucket: options.isProtected ? this.private_bucket_ : this.bucket_,
|
||||
Body: fs.createReadStream(file.path),
|
||||
Key: fileKey,
|
||||
ContentType: file.mimetype,
|
||||
}
|
||||
|
||||
const result = await client.upload(params).promise()
|
||||
|
||||
return { url: result.Location, key: result.Key }
|
||||
}
|
||||
|
||||
async delete(file: DeleteFileType): Promise<void> {
|
||||
const privateClient = this.getClient(false)
|
||||
const publicClient = this.getClient(true)
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${file.fileKey}`,
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) =>
|
||||
publicClient.deleteObject(
|
||||
{ ...params, Bucket: this.bucket_ },
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
}
|
||||
)
|
||||
),
|
||||
new Promise((resolve, reject) =>
|
||||
privateClient.deleteObject(
|
||||
{ ...params, Bucket: this.private_bucket_ },
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
}
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
async getUploadStreamDescriptor(
|
||||
fileData: UploadStreamDescriptorType & {
|
||||
usePrivateBucket?: boolean
|
||||
contentType?: string
|
||||
}
|
||||
) {
|
||||
const usePrivateBucket = !!fileData.usePrivateBucket
|
||||
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
|
||||
const client = this.getClient(usePrivateBucket)
|
||||
|
||||
const pass = new stream.PassThrough()
|
||||
|
||||
const fileKey = `${fileData.name}.${fileData.ext}`
|
||||
|
||||
const params: PutObjectRequest = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Body: pass,
|
||||
Key: fileKey,
|
||||
ContentType: fileData.contentType,
|
||||
}
|
||||
|
||||
return {
|
||||
writeStream: pass,
|
||||
promise: client.upload(params).promise(),
|
||||
url: this.buildUrl(params.Bucket, fileKey),
|
||||
fileKey,
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadStream(
|
||||
fileData: GetUploadedFileType & { usePrivateBucket?: boolean }
|
||||
) {
|
||||
const usePrivateBucket = !!fileData.usePrivateBucket
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
const client = this.getClient(usePrivateBucket)
|
||||
|
||||
const params = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
}
|
||||
|
||||
return client.getObject(params).createReadStream()
|
||||
}
|
||||
|
||||
async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) {
|
||||
this.validatePrivateBucketConfiguration_(usePrivateBucket)
|
||||
const client = this.getClient(usePrivateBucket, {
|
||||
signatureVersion: "v4",
|
||||
})
|
||||
|
||||
const params = {
|
||||
Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
Expires: this.downloadUrlDuration,
|
||||
}
|
||||
|
||||
return await client.getSignedUrlPromise("getObject", params)
|
||||
}
|
||||
|
||||
validatePrivateBucketConfiguration_(usePrivateBucket) {
|
||||
if (
|
||||
usePrivateBucket &&
|
||||
(!this.private_access_key_id_ || !this.private_bucket_)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Private bucket is not configured"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected getClient(
|
||||
usePrivateBucket = false,
|
||||
additionalConfiguration: Partial<ClientConfiguration> = {}
|
||||
) {
|
||||
return new aws.S3({
|
||||
accessKeyId: usePrivateBucket
|
||||
? this.private_access_key_id_
|
||||
: this.accessKeyId_,
|
||||
secretAccessKey: usePrivateBucket
|
||||
? this.private_secret_access_key_
|
||||
: this.secretAccessKey_,
|
||||
endpoint: this.endpoint_,
|
||||
s3ForcePathStyle: this.s3ForcePathStyle_,
|
||||
signatureVersion: this.signatureVersion_,
|
||||
...additionalConfiguration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default MinioService
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es5", "es6", "es2019"],
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "src",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -23,37 +23,38 @@ Store uploaded files to your Medusa backend on S3.
|
||||
|
||||
1\. Run the following command in the directory of the Medusa backend:
|
||||
|
||||
```bash
|
||||
npm install medusa-file-s3
|
||||
```
|
||||
```bash
|
||||
npm install medusa-file-s3
|
||||
```
|
||||
|
||||
2\. Set the following environment variables in `.env`:
|
||||
|
||||
```bash
|
||||
S3_URL=<YOUR_BUCKET_URL>
|
||||
S3_BUCKET=<YOUR_BUCKET_NAME>
|
||||
S3_REGION=<YOUR_BUCKET_REGION>
|
||||
S3_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
|
||||
S3_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
|
||||
```
|
||||
```bash
|
||||
S3_URL=<YOUR_BUCKET_URL>
|
||||
S3_BUCKET=<YOUR_BUCKET_NAME>
|
||||
S3_REGION=<YOUR_BUCKET_REGION>
|
||||
S3_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
|
||||
S3_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
|
||||
```
|
||||
|
||||
3\. In `medusa-config.js` add the following at the end of the `plugins` array:
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-s3`,
|
||||
options: {
|
||||
s3_url: process.env.S3_URL,
|
||||
bucket: process.env.S3_BUCKET,
|
||||
region: process.env.S3_REGION,
|
||||
access_key_id: process.env.S3_ACCESS_KEY_ID,
|
||||
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
```js
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-file-s3`,
|
||||
options: {
|
||||
s3_url: process.env.S3_URL,
|
||||
bucket: process.env.S3_BUCKET,
|
||||
region: process.env.S3_REGION,
|
||||
access_key_id: process.env.S3_ACCESS_KEY_ID,
|
||||
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
|
||||
aws_config_option: {},
|
||||
},
|
||||
]
|
||||
```
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -61,9 +62,9 @@ Store uploaded files to your Medusa backend on S3.
|
||||
|
||||
1\. Run the following command in the directory of the Medusa backend to run the backend:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
2\. Upload an image for a product using the admin dashboard or using [the Admin APIs](https://docs.medusajs.com/api/admin#tag/Upload).
|
||||
|
||||
@@ -71,4 +72,4 @@ Store uploaded files to your Medusa backend on S3.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3)
|
||||
- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3)
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"name": "medusa-file-s3",
|
||||
"version": "1.1.12",
|
||||
"description": "AWS s3 file connector for Medusa",
|
||||
"main": "index.js",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
@@ -11,32 +14,23 @@
|
||||
"author": "Sebastian Mateos Nicolajsen",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"@medusajs/medusa": "1.8.0-rc.6",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"medusa-interfaces": "^1.3.7",
|
||||
"medusa-test-utils": "^1.1.40"
|
||||
"medusa-test-utils": "^1.1.40",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"test": "jest --passWithNoTests src",
|
||||
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
|
||||
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.3.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.15.4",
|
||||
"aws-sdk": "^2.983.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import fs from "fs"
|
||||
import aws from "aws-sdk"
|
||||
import { parse } from "path"
|
||||
import { AbstractFileService } from "@medusajs/medusa"
|
||||
import stream from "stream"
|
||||
|
||||
class S3Service extends AbstractFileService {
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
constructor({}, options) {
|
||||
super({}, options)
|
||||
|
||||
this.bucket_ = options.bucket
|
||||
this.s3Url_ = options.s3_url
|
||||
this.accessKeyId_ = options.access_key_id
|
||||
this.secretAccessKey_ = options.secret_access_key
|
||||
this.region_ = options.region
|
||||
this.endpoint_ = options.endpoint
|
||||
this.awsConfigObject_ = options.aws_config_object
|
||||
|
||||
this.client_ = new aws.S3()
|
||||
}
|
||||
|
||||
upload(file) {
|
||||
this.updateAwsConfig()
|
||||
|
||||
return this.uploadFile(file)
|
||||
}
|
||||
|
||||
uploadProtected(file) {
|
||||
this.updateAwsConfig()
|
||||
|
||||
return this.uploadFile(file, { acl: "private" })
|
||||
}
|
||||
|
||||
uploadFile(file, options = { isProtected: false, acl: undefined }) {
|
||||
const parsedFilename = parse(file.originalname)
|
||||
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
|
||||
|
||||
const params = {
|
||||
ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
|
||||
Bucket: this.bucket_,
|
||||
Body: fs.createReadStream(file.path),
|
||||
Key: fileKey,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client_.upload(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
resolve({ url: data.Location, key: data.Key })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async delete(file) {
|
||||
this.updateAwsConfig()
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${file}`,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client_.deleteObject(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getUploadStreamDescriptor(fileData) {
|
||||
this.updateAwsConfig()
|
||||
|
||||
const pass = new stream.PassThrough()
|
||||
|
||||
const fileKey = `${fileData.name}.${fileData.ext}`
|
||||
const params = {
|
||||
ACL: fileData.acl ?? "private",
|
||||
Bucket: this.bucket_,
|
||||
Body: pass,
|
||||
Key: fileKey,
|
||||
}
|
||||
|
||||
return {
|
||||
writeStream: pass,
|
||||
promise: this.client_.upload(params).promise(),
|
||||
url: `${this.s3Url_}/${fileKey}`,
|
||||
fileKey,
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadStream(fileData) {
|
||||
this.updateAwsConfig()
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
}
|
||||
|
||||
return this.client_.getObject(params).createReadStream()
|
||||
}
|
||||
|
||||
async getPresignedDownloadUrl(fileData) {
|
||||
this.updateAwsConfig({
|
||||
signatureVersion: "v4",
|
||||
})
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
Expires: this.downloadUrlDuration,
|
||||
}
|
||||
|
||||
return await this.client_.getSignedUrlPromise("getObject", params)
|
||||
}
|
||||
|
||||
updateAwsConfig(additionalConfiguration = {}) {
|
||||
aws.config.setPromisesDependency(null)
|
||||
|
||||
const config = {
|
||||
...additionalConfiguration,
|
||||
accessKeyId: this.accessKeyId_,
|
||||
secretAccessKey: this.secretAccessKey_,
|
||||
region: this.region_,
|
||||
endpoint: this.endpoint_,
|
||||
...this.awsConfigObject_,
|
||||
}
|
||||
|
||||
aws.config.update(config, true)
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Service
|
||||
@@ -0,0 +1,157 @@
|
||||
import fs from "fs"
|
||||
import aws from "aws-sdk"
|
||||
import { parse } from "path"
|
||||
import {
|
||||
AbstractFileService,
|
||||
DeleteFileType,
|
||||
FileServiceUploadResult,
|
||||
GetUploadedFileType,
|
||||
IFileService,
|
||||
UploadStreamDescriptorType,
|
||||
} from "@medusajs/medusa"
|
||||
import stream from "stream"
|
||||
import { PutObjectRequest } from "aws-sdk/clients/s3"
|
||||
import { ClientConfiguration } from "aws-sdk/clients/s3"
|
||||
|
||||
class S3Service extends AbstractFileService implements IFileService {
|
||||
protected bucket_: string
|
||||
protected s3Url_: string
|
||||
protected accessKeyId_: string
|
||||
protected secretAccessKey_: string
|
||||
protected region_: string
|
||||
protected endpoint_: string
|
||||
protected awsConfigObject_: any
|
||||
protected downloadFileDuration_: string
|
||||
|
||||
constructor({}, options) {
|
||||
super({}, options)
|
||||
|
||||
this.bucket_ = options.bucket
|
||||
this.s3Url_ = options.s3_url
|
||||
this.accessKeyId_ = options.access_key_id
|
||||
this.secretAccessKey_ = options.secret_access_key
|
||||
this.region_ = options.region
|
||||
this.endpoint_ = options.endpoint
|
||||
this.downloadFileDuration_ = options.download_file_duration
|
||||
this.awsConfigObject_ = options.aws_config_object ?? {}
|
||||
}
|
||||
|
||||
protected getClient(overwriteConfig: Partial<ClientConfiguration> = {}) {
|
||||
const config: ClientConfiguration = {
|
||||
accessKeyId: this.accessKeyId_,
|
||||
secretAccessKey: this.secretAccessKey_,
|
||||
region: this.region_,
|
||||
endpoint: this.endpoint_,
|
||||
...this.awsConfigObject_,
|
||||
...overwriteConfig,
|
||||
}
|
||||
|
||||
return new aws.S3(config)
|
||||
}
|
||||
|
||||
async upload(file: Express.Multer.File): Promise<FileServiceUploadResult> {
|
||||
return await this.uploadFile(file)
|
||||
}
|
||||
|
||||
async uploadProtected(file: Express.Multer.File) {
|
||||
return await this.uploadFile(file, { acl: "private" })
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
file: Express.Multer.File,
|
||||
options: { isProtected?: boolean; acl?: string } = {
|
||||
isProtected: false,
|
||||
acl: undefined,
|
||||
}
|
||||
) {
|
||||
const client = this.getClient()
|
||||
|
||||
const parsedFilename = parse(file.originalname)
|
||||
|
||||
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
|
||||
|
||||
const params = {
|
||||
ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
|
||||
Bucket: this.bucket_,
|
||||
Body: fs.createReadStream(file.path),
|
||||
Key: fileKey,
|
||||
ContentType: file.mimetype,
|
||||
}
|
||||
|
||||
const result = await client.upload(params).promise()
|
||||
|
||||
return {
|
||||
url: result.Location,
|
||||
key: result.Key,
|
||||
}
|
||||
}
|
||||
|
||||
async delete(file: DeleteFileType): Promise<void> {
|
||||
const client = this.getClient()
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${file}`,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
client.deleteObject(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getUploadStreamDescriptor(fileData: UploadStreamDescriptorType) {
|
||||
const client = this.getClient()
|
||||
const pass = new stream.PassThrough()
|
||||
|
||||
const fileKey = `${fileData.name}.${fileData.ext}`
|
||||
const params: PutObjectRequest = {
|
||||
ACL: fileData.acl ?? "private",
|
||||
Bucket: this.bucket_,
|
||||
Body: pass,
|
||||
Key: fileKey,
|
||||
ContentType: fileData.contentType as string,
|
||||
}
|
||||
|
||||
return {
|
||||
writeStream: pass,
|
||||
promise: client.upload(params).promise(),
|
||||
url: `${this.s3Url_}/${fileKey}`,
|
||||
fileKey,
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadStream(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<NodeJS.ReadableStream> {
|
||||
const client = this.getClient()
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
}
|
||||
|
||||
return await client.getObject(params).createReadStream()
|
||||
}
|
||||
|
||||
async getPresignedDownloadUrl(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<string> {
|
||||
const client = this.getClient({ signatureVersion: "v4" })
|
||||
|
||||
const params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${fileData.fileKey}`,
|
||||
Expires: this.downloadFileDuration_,
|
||||
}
|
||||
|
||||
return await client.getSignedUrlPromise("getObject", params)
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Service
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es5", "es6", "es2019"],
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "src",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user