Created
October 22, 2023 12:21
-
-
Save wmakeev/2d2a27900e30875f975fab945a064b31 to your computer and use it in GitHub Desktop.
[S3BucketStorage] #s3 #bucket #upload #storage #tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
S3Client, | |
PutObjectCommand, | |
GetObjectCommand, | |
HeadObjectCommand | |
} from '@aws-sdk/client-s3' | |
import { getSignedUrl } from '@aws-sdk/s3-request-presigner' | |
import path from 'node:path' | |
import mime from 'mime-types' | |
import querystring from 'node:querystring' | |
import { createReadStream } from 'node:fs' | |
import assert from 'node:assert' | |
/** | |
* @typedef {Object} StorageConfig | |
* @property {string} bucket | |
* @property {string} endpoint | |
* @property {string} accessKeyId | |
* @property {string} secretAccessKey | |
* @property {string} region | |
*/ | |
/** | |
* @typedef {Object} PutObjectParams | |
* @property {string} objectKey | |
* @property {import('@aws-sdk/client-s3').PutObjectCommandInput['Body']} body | |
* @property {string} [contentDisposition] | |
* @property {string} [mimeType] | |
* @property {Date} [expires] | |
* @property {Record<string, string>} [tags] | |
* @property {import('@aws-sdk/client-s3').PutObjectCommandInput['ACL']} [ACL] | |
* @property {import('@aws-sdk/client-s3').PutObjectCommandInput['StorageClass']} [storageClass] | |
* | |
*/ | |
/** | |
* @typedef {Omit<PutObjectParams, 'body' | 'objectKey' | 'mimeType'> & { downloadFileName?: string; fileExtension?: string }} UploadOptions | |
*/ | |
export class S3BucketStorage { | |
/** | |
* @param {StorageConfig} config | |
*/ | |
constructor(config) { | |
this.config = config | |
const { endpoint, accessKeyId, secretAccessKey, region } = config | |
this.client = new S3Client({ | |
endpoint, | |
forcePathStyle: true, | |
apiVersion: '2006-03-01', | |
credentials: { | |
accessKeyId, | |
secretAccessKey | |
}, | |
region | |
}) | |
} | |
/** | |
* @param {string} objectKey | |
*/ | |
async headObject(objectKey) { | |
const result = await this.client.send( | |
new HeadObjectCommand({ | |
Bucket: this.config.bucket, | |
Key: objectKey | |
}) | |
) | |
return result | |
} | |
/** | |
* | |
* @param {PutObjectParams} params | |
*/ | |
async putObject(params) { | |
const { | |
objectKey, | |
body, | |
ACL, | |
storageClass, | |
contentDisposition, | |
mimeType, | |
expires, | |
tags | |
} = params | |
const tagging = tags ? querystring.stringify(tags) : undefined | |
const result = await this.client.send( | |
new PutObjectCommand({ | |
Bucket: this.config.bucket, | |
Key: objectKey, | |
Body: body, | |
ACL, | |
StorageClass: storageClass ?? 'STANDARD', | |
ContentDisposition: contentDisposition, | |
ContentType: mimeType, | |
Expires: expires, | |
Tagging: tagging | |
}) | |
) | |
return result | |
} | |
/** | |
* Загрузить файл в хранилище | |
* | |
* @param {string} file Путь к файлу для загрузки | |
* @param {string} objectKey Ключ (путь) файла в хранилище | |
* @param {UploadOptions} [options] Опции | |
*/ | |
async uploadFile(file, objectKey, options = {}) { | |
const { | |
downloadFileName = path.basename(file), | |
fileExtension, | |
expires, | |
tags, | |
ACL | |
} = options | |
const mimeType = | |
mime.lookup(fileExtension ?? downloadFileName) || | |
'application/octet-stream' | |
const contentDisposition = downloadFileName | |
? `attachment; filename="${downloadFileName}"` | |
: undefined | |
const fileStream = createReadStream(file) | |
const result = await this.putObject({ | |
body: fileStream, | |
objectKey, | |
ACL, | |
contentDisposition, | |
expires, | |
mimeType, | |
tags | |
}) | |
return result | |
} | |
/** | |
* Загрузить буфер в хранилище | |
* | |
* @param {Buffer} buffer Путь к файлу для загрузки | |
* @param {string} objectKey Ключ (путь) файла в хранилище | |
* @param {UploadOptions} [options] Опции | |
*/ | |
async upload(buffer, objectKey, options = {}) { | |
const { downloadFileName, fileExtension, expires, tags, ACL } = options | |
try { | |
const headResult = await this.headObject(objectKey) | |
return headResult | |
} catch (err) { | |
assert.ok(err instanceof Error) | |
if (err.name !== 'NotFound') { | |
throw err | |
} | |
} | |
const mimeSource = fileExtension ?? downloadFileName | |
const mimeType = | |
(mimeSource ? mime.lookup(mimeSource) : false) || | |
'application/octet-stream' | |
const contentDisposition = downloadFileName | |
? `attachment; filename="${downloadFileName}"` | |
: undefined | |
const putResult = await this.putObject({ | |
body: buffer, | |
objectKey, | |
ACL, | |
contentDisposition, | |
expires, | |
mimeType, | |
tags | |
}) | |
return putResult | |
} | |
/** | |
* Получить публичную ссылку на скачивание файла | |
* | |
* @param {string} objectKey Ключ (путь) файла в хранилище | |
* @param {Object} options Опции | |
* @param {number} options.expiresIn The number of seconds before the presigned URL expires | |
*/ | |
async getPublicDownloadUrl(objectKey, options) { | |
const { expiresIn } = options | |
const command = new GetObjectCommand({ | |
Bucket: this.config.bucket, | |
Key: objectKey | |
}) | |
const url = await getSignedUrl(this.client, command, { expiresIn }) | |
return url | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment