Files
speckle-server/packages/server/modules/blobstorage/repositories/blobs.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

158 lines
4.7 KiB
TypeScript

import { ObjectStorage } from '@/modules/blobstorage/clients/objectStorage'
import {
DeleteObject,
EnsureStorageAccess,
GetObjectAttributes,
GetObjectStream,
StoreFileStream
} from '@/modules/blobstorage/domain/storageOperations'
import {
BadRequestError,
EnvironmentResourceError,
NotFoundError
} from '@/modules/shared/errors'
import {
CreateBucketCommand,
DeleteObjectCommand,
GetObjectCommand,
HeadBucketCommand,
S3ServiceException,
ServiceOutputTypes
} from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import type { Command } from '@aws-sdk/smithy-client'
import { ensureError } from '@speckle/shared'
import { get } from 'lodash-es'
import type stream from 'stream'
const sendCommand = async <CommandOutput extends ServiceOutputTypes>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
command: Command<any, CommandOutput, any, any, any>,
storage: ObjectStorage
) => {
const { client } = storage
try {
const ret = await client.send(command)
return ret
} catch (err) {
if (err instanceof S3ServiceException && get(err, 'Code') === 'NoSuchKey')
throw new NotFoundError(err.message)
throw err
}
}
export const getObjectStreamFactory =
(deps: { storage: ObjectStorage }): GetObjectStream =>
async ({ objectKey }) => {
const { storage } = deps
const data = await sendCommand(
new GetObjectCommand({ Bucket: storage.bucket, Key: objectKey }),
storage
)
// Apparently not always stream.Readable according to types, but in practice it always is
return data.Body as stream.Readable
}
export const getObjectAttributesFactory =
(deps: { storage: ObjectStorage }): GetObjectAttributes =>
async ({ objectKey }) => {
const { storage } = deps
const data = await sendCommand(
new GetObjectCommand({ Bucket: storage.bucket, Key: objectKey }),
storage
)
return { fileSize: data.ContentLength || 0 }
}
export const storeFileStreamFactory =
(deps: { storage: ObjectStorage }): StoreFileStream =>
async ({ objectKey, fileStream }) => {
const {
storage: { client, bucket: Bucket }
} = deps
const upload = 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
})
const data = await upload.done()
// the ETag is a hash of the object. Could be used to dedupe stuff...
if (!data || !('ETag' in data) || !data.ETag) {
throw new BadRequestError('No ETag in response')
}
const fileHash = data.ETag.replaceAll('"', '')
return { fileHash }
}
export const deleteObjectFactory =
(deps: { storage: ObjectStorage }): DeleteObject =>
async ({ objectKey }) => {
await sendCommand(
new DeleteObjectCommand({ Bucket: deps.storage.bucket, Key: objectKey }),
deps.storage
)
}
// No idea what the actual error type is, too difficult to figure out
type EnsureStorageAccessError = Error & {
statusCode?: number
$metadata?: { httpStatusCode?: number }
}
const isExpectedEnsureStorageAccessError = (
err: unknown
): err is EnsureStorageAccessError =>
err instanceof Error && ('statusCode' in err || '$metadata' in err)
export const ensureStorageAccessFactory =
(deps: { storage: ObjectStorage }): EnsureStorageAccess =>
async ({ createBucketIfNotExists }) => {
const { client, bucket: Bucket } = deps.storage
try {
await client.send(new HeadBucketCommand({ Bucket }))
return
} catch (err) {
if (
isExpectedEnsureStorageAccessError(err) &&
(err.statusCode === 403 || err['$metadata']?.httpStatusCode === 403)
) {
throw new EnvironmentResourceError("Access denied to S3 bucket '{bucket}'", {
cause: err,
info: { bucket: Bucket }
})
}
if (createBucketIfNotExists) {
try {
await client.send(new CreateBucketCommand({ Bucket }))
} catch (err) {
throw new EnvironmentResourceError(
"Can't open S3 bucket '{bucket}', and have failed to create it.",
{
cause: ensureError(err),
info: { bucket: Bucket }
}
)
}
} else {
throw new EnvironmentResourceError(
"Can't open S3 bucket '{bucket}', and the Speckle server configuration has disabled creation of the bucket.",
{
cause: ensureError(err),
info: { bucket: Bucket }
}
)
}
}
}