Files
speckle-server/packages/server/modules/blobstorage/services.js
T
Gergő Jedlicska ed458fb619 Add blob storage backend (#802)
* feat(server): add server authz pipeline rework first sketch

* feat(server authz): add new server authz middleware poc implementation

* test(server authz): add unittests for the new server authz workflow

* feat(wip rework of fileuploads vs blob storage): add basim impl of separate blob storage service

* feat(fileimport service): refactored file import service to utilize the new asssetstorage service

* refactor(server errors): refactor server errors to use the shared module definitions

Now all the errors inherit from BaseError

* refactor(fileimport service): cleanup after refactor

* feat(frontend fileimports): use the new blob storage for downloading the original file

* refactor(server fileimports): clean up the remnants of S3 storage from file imports

* refactor(server authz): centralize generic authz pipeline configs

* refactor(server blob storage): refactor / rename everything to use the `blob-storage` name

* ci(circleci): add s3 objectstorage environment variables

* ci(circleci): fix missing env variables

* ci(circleci): add minio test container

* ci(circleci): fix minio app startup

* ci(circleci): enable circleci remote docker

* ci(circleci): fix minio startup

* ci(cirleci): detach and wait properly for minio to start

* ci(circleci): revert to additional minio img config, it only fails when the container is stopped ?!

* ci(circleci): disable file uploads

* fix(fileimports): update with blob storage refactor leftovers

* feat(server blob storage): add blob storage graphql api

* refactor(server errors): merge new errors to shared module

* fix(server comments rte): fix import for RTE error

* chore(fileimports): remove node-fetch from dependency

* chore(server): remove body parser dependency

* fix(server blob storage): fix gql api

* fix(frontend): fix fileupload item not loading the new upload status, cause of premature event fire

* feat(server blob storage): fix file size limit and allow for public streams

* Update packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>

* chore(blobstorage): fix PR review issues

* fix(server): fix import bugs

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
2022-06-16 11:31:03 +02:00

108 lines
3.6 KiB
JavaScript

const knex = require('@/db/knex')
const { NotFoundError, ResourceMismatch } = require('@/modules/shared/errors')
const BlobStorage = () => knex('blob_storage')
const blobLookup = ({ blobId }) => BlobStorage().where({ id: blobId })
const uploadFileStream = async (
storeFileStream,
{ streamId, userId },
{ blobId, fileName, fileType, fileStream }
) => {
const objectKey = `assets/${streamId}/${blobId}`
const dbFile = {
id: blobId,
streamId,
userId,
objectKey,
fileName,
fileType
}
// need to insert the upload data before starting otherwise the upload finished
// even might fire faster, than the db insert, causing missing asset data in the db
await BlobStorage().insert(dbFile)
const { fileHash } = await storeFileStream({ objectKey, fileStream })
return { blobId, fileName, fileHash }
}
const getBlobMetadata = async ({ streamId, blobId }) => {
const obj = (await blobLookup({ blobId }).first()) || null
if (!obj) throw new NotFoundError(`The requested asset: ${blobId} doesn't exist`)
if (!streamId) throw new ResourceMismatch('No steamId provided')
if (obj.streamId !== streamId)
throw new ResourceMismatch("The stream doesn't have the given resource")
return obj
}
const blobQuery = ({ streamId, query }) => {
let blobs = BlobStorage().where({ streamId })
if (query) blobs = blobs.andWhereLike('fileName', `%${query}%`)
return blobs
}
const getBlobMetadataCollection = async ({ streamId, query, limit, cursor }) => {
const cursorTarget = 'createdAt'
const limitMax = 25
const queryLimit = limit && limit < limitMax ? limit : limitMax
const blobs = blobQuery({ streamId, query })
.orderBy(cursorTarget, 'desc')
.limit(queryLimit)
if (cursor) query.andWhere(cursorTarget, '<', cursor)
const rows = await blobs
return {
blobs: rows,
cursor: rows.length > 0 ? rows[rows.length - 1][cursorTarget].toISOString() : null
}
}
const blobCollectionSummary = async ({ streamId, query }) => {
const [summary] = await blobQuery({ streamId, query }).sum('fileSize').count('id')
return { totalSize: summary.sum ?? 0, totalCount: summary.count }
}
const getFileStream = async ({ getObjectStream, streamId, blobId }) => {
const { objectKey } = await getBlobMetadata({ streamId, blobId })
return await getObjectStream({ objectKey })
}
const markUploadSuccess = async (getObjectAttributes, streamId, blobId) =>
await updateBlobMetadata(streamId, blobId, async ({ objectKey }) => {
const { fileSize } = await getObjectAttributes({ objectKey })
return { uploadStatus: 1, fileSize }
})
const markUploadOverFileSizeLimit = async (deleteObject, streamId, blobId) =>
await markUploadError(deleteObject, streamId, blobId, 'File size limit reached')
const markUploadError = async (deleteObject, streamId, blobId, error) =>
await updateBlobMetadata(streamId, blobId, async ({ objectKey }) => {
await deleteObject({ objectKey })
return { uploadStatus: 2, uploadError: error }
})
const deleteBlob = async ({ streamId, blobId, deleteObject }) => {
const { objectKey } = await getBlobMetadata({ streamId, blobId })
await deleteObject({ objectKey })
await blobLookup({ blobId }).del()
}
const updateBlobMetadata = async (streamId, blobId, updateCallback) => {
const { objectKey, fileName } = await getBlobMetadata({ streamId, blobId })
const updateData = await updateCallback({ objectKey })
await blobLookup({ blobId }).update(updateData)
return { blobId, fileName, ...updateData }
}
module.exports = {
getBlobMetadata,
uploadFileStream,
markUploadSuccess,
markUploadOverFileSizeLimit,
markUploadError,
getFileStream,
deleteBlob,
getBlobMetadataCollection,
blobCollectionSummary
}