Files
speckle-server/packages/server/modules/blobstorage/objectStorage.js
T
Peter Grainger 72d27b9a7c Allow save object to S3 in different region (#910)
* Allow save object to S3 in different region

* feat(helm & docker-compose): adds S3_REGION to helm chart & docker-compose

Explicitly adding the environment variable to deployment configuration files provides system operators with documentation of its existence.

Set to empty by default, which will result in the default value being used.

Co-authored-by: Iain Sproat <68657+iainsproat@users.noreply.github.com>
2022-08-15 14:24:30 +02:00

132 lines
3.6 KiB
JavaScript

const { NotFoundError } = require('@/modules/shared/errors')
const {
S3Client,
GetObjectCommand,
HeadBucketCommand,
DeleteObjectCommand,
CreateBucketCommand,
S3ServiceException
} = require('@aws-sdk/client-s3')
const { Upload } = require('@aws-sdk/lib-storage')
let s3Config = null
const getS3Config = () => {
if (!s3Config) {
if (!process.env.S3_ACCESS_KEY)
throw new Error('Config value S3_ACCESS_KEY is missing')
if (!process.env.S3_SECRET_KEY)
throw new Error('Config value S3_SECRET_KEY is missing')
if (!process.env.S3_ENDPOINT) throw new Error('Config value S3_ENDPOINT is missing')
s3Config = {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY
},
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: true,
// s3ForcePathStyle: true,
// signatureVersion: 'v4',
region: process.env.S3_REGION || 'us-east-1'
}
}
return s3Config
}
let storageBucket = null
const getStorageBucket = () => {
if (!storageBucket) {
if (!process.env.S3_BUCKET) throw new Error('Config value S3_BUCKET is missing')
storageBucket = process.env.S3_BUCKET
}
return storageBucket
}
const getObjectStorage = () => ({
client: new S3Client(getS3Config()),
Bucket: getStorageBucket(),
createBucket: process.env.S3_CREATE_BUCKET || false
})
const sendCommand = async (command) => {
const { client, Bucket } = getObjectStorage()
try {
return await client.send(command(Bucket))
} catch (err) {
if (err instanceof S3ServiceException && err.Code === 'NoSuchKey')
throw new NotFoundError(err.message)
throw err
}
}
const getObjectStream = async ({ objectKey }) => {
const data = await sendCommand(
(Bucket) => new GetObjectCommand({ Bucket, Key: objectKey })
)
return data.Body
}
const getObjectAttributes = async ({ objectKey }) => {
const data = await sendCommand(
(Bucket) => new GetObjectCommand({ Bucket, Key: objectKey })
)
return { fileSize: data.ContentLength }
}
const storeFileStream = async ({ objectKey, fileStream }) => {
const { client, Bucket } = getObjectStorage()
const parallelUploads3 = new Upload({
client,
params: { Bucket, Key: objectKey, Body: fileStream },
tags: [
/*...*/
], // optional tags
queueSize: 4, // optional concurrency configuration
partSize: 1024 * 1024 * 5, // optional size of each part, in bytes, at least 5MB
leavePartsOnError: false // optional manually handle dropped parts
})
// parallelUploads3.on('httpUploadProgress', (progress) => {
// console.log(progress)
// })
const data = await parallelUploads3.done()
// the ETag is a hash of the object. Could be used to dedupe stuff...
const fileHash = data.ETag.replaceAll('"', '')
return { fileHash }
}
const deleteObject = async ({ objectKey }) => {
await sendCommand((Bucket) => new DeleteObjectCommand({ Bucket, Key: objectKey }))
}
const ensureStorageAccess = async () => {
const { client, Bucket, createBucket } = getObjectStorage()
try {
// await this._client.send(new HeadBucketCommand({ Bucket: this._bucket }))
await client.send(new HeadBucketCommand({ Bucket }))
return
} catch (err) {
if (err.statusCode === 403) {
throw new Error('Access denied to S3 bucket ')
}
if (createBucket) {
try {
await client.send(new CreateBucketCommand({ Bucket }))
} catch (err) {
console.log(err)
}
} else {
throw new Error(`Can't open S3 bucket '${Bucket}': ${err.toString()}`)
}
}
}
module.exports = {
ensureStorageAccess,
deleteObject,
getObjectAttributes,
storeFileStream,
getObjectStream
}