feat(server): add support for b2 object storage type (#232)

* feat(b2): add support for b2 object storage type

* feat(b2): fix order of tsconfig entries

* feat(b2): fix accidental responseType change

* fix(b2): remove unnecessary try-catches

* refactor(b2): use error factories
This commit is contained in:
Joshua Anderson
2025-04-27 13:35:29 -06:00
committed by GitHub
parent 59ba9465f6
commit 096331a4ee
6 changed files with 136 additions and 2 deletions

View File

@@ -41,6 +41,7 @@
"@owlrelay/webhook": "^0.0.3",
"@papra/lecture": "^0.0.4",
"@paralleldrive/cuid2": "^2.2.2",
"backblaze-b2": "^1.7.0",
"better-auth": "catalog:",
"c12": "^3.0.2",
"chokidar": "^4.0.3",
@@ -66,6 +67,7 @@
"@antfu/eslint-config": "catalog:",
"@crowlog/pretty": "^1.1.1",
"@total-typescript/ts-reset": "^0.6.1",
"@types/backblaze-b2": "^1.5.6",
"@types/lodash-es": "^4.17.12",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.10.2",

View File

@@ -1,5 +1,6 @@
import type { ConfigDefinition } from 'figue';
import { z } from 'zod';
import { B2_STORAGE_DRIVER_NAME } from './drivers/b2/b2.storage-driver';
import { FS_STORAGE_DRIVER_NAME } from './drivers/fs/fs.storage-driver';
import { IN_MEMORY_STORAGE_DRIVER_NAME } from './drivers/memory/memory.storage-driver';
import { S3_STORAGE_DRIVER_NAME } from './drivers/s3/s3.storage-driver';
@@ -12,8 +13,8 @@ export const documentStorageConfig = {
env: 'DOCUMENT_STORAGE_MAX_UPLOAD_SIZE',
},
driver: {
doc: `The driver to use for document storage, values can be one of: ${[FS_STORAGE_DRIVER_NAME, S3_STORAGE_DRIVER_NAME, IN_MEMORY_STORAGE_DRIVER_NAME].map(x => `\`${x}\``).join(', ')}`,
schema: z.enum([FS_STORAGE_DRIVER_NAME, S3_STORAGE_DRIVER_NAME, IN_MEMORY_STORAGE_DRIVER_NAME]),
doc: `The driver to use for document storage, values can be one of: ${[FS_STORAGE_DRIVER_NAME, S3_STORAGE_DRIVER_NAME, IN_MEMORY_STORAGE_DRIVER_NAME, B2_STORAGE_DRIVER_NAME].map(x => `\`${x}\``).join(', ')}`,
schema: z.enum([FS_STORAGE_DRIVER_NAME, S3_STORAGE_DRIVER_NAME, IN_MEMORY_STORAGE_DRIVER_NAME, B2_STORAGE_DRIVER_NAME]),
default: FS_STORAGE_DRIVER_NAME,
env: 'DOCUMENT_STORAGE_DRIVER',
},
@@ -58,5 +59,31 @@ export const documentStorageConfig = {
env: 'DOCUMENT_STORAGE_S3_ENDPOINT',
},
},
b2: {
applicationKeyId: {
doc: 'The B2 application key ID',
schema: z.string(),
default: '',
env: 'DOCUMENT_STORAGE_B2_APPLICATION_KEY_ID',
},
applicationKey: {
doc: 'The B2 application key',
schema: z.string(),
default: '',
env: 'DOCUMENT_STORAGE_B2_APPLICATION_KEY',
},
bucketName: {
doc: 'The B2 bucket name',
schema: z.string(),
default: '',
env: 'DOCUMENT_STORAGE_B2_BUCKET_NAME',
},
bucketId: {
doc: 'The B2 bucket ID',
schema: z.string(),
default: '',
env: 'DOCUMENT_STORAGE_B2_BUCKET_ID',
},
},
},
} as const satisfies ConfigDefinition;

View File

@@ -1,5 +1,6 @@
import type { Config } from '../../config/config.types';
import { createError } from '../../shared/errors/errors';
import { B2_STORAGE_DRIVER_NAME, b2StorageDriverFactory } from './drivers/b2/b2.storage-driver';
import { FS_STORAGE_DRIVER_NAME, fsStorageDriverFactory } from './drivers/fs/fs.storage-driver';
import { IN_MEMORY_STORAGE_DRIVER_NAME, inMemoryStorageDriverFactory } from './drivers/memory/memory.storage-driver';
import { S3_STORAGE_DRIVER_NAME, s3StorageDriverFactory } from './drivers/s3/s3.storage-driver';
@@ -8,6 +9,7 @@ const storageDriverFactories = {
[FS_STORAGE_DRIVER_NAME]: fsStorageDriverFactory,
[S3_STORAGE_DRIVER_NAME]: s3StorageDriverFactory,
[IN_MEMORY_STORAGE_DRIVER_NAME]: inMemoryStorageDriverFactory,
[B2_STORAGE_DRIVER_NAME]: b2StorageDriverFactory,
};
export type DocumentStorageService = Awaited<ReturnType<typeof createDocumentStorageService>>;

View File

@@ -0,0 +1,54 @@
import { Buffer } from 'node:buffer';
import B2 from 'backblaze-b2';
import { createFileNotFoundError } from '../../document-storage.errors';
import { defineStorageDriver } from '../drivers.models';
export const B2_STORAGE_DRIVER_NAME = 'b2' as const;
export const b2StorageDriverFactory = defineStorageDriver(async ({ config }) => {
const { applicationKeyId, applicationKey, bucketId, bucketName } = config.documentsStorage.drivers.b2;
const b2Client = new B2({
applicationKey,
applicationKeyId,
});
return {
name: B2_STORAGE_DRIVER_NAME,
saveFile: async ({ file, storageKey }) => {
await b2Client.authorize();
const getUploadUrl = await b2Client.getUploadUrl({
bucketId,
});
const upload = await b2Client.uploadFile({
uploadUrl: getUploadUrl.data.uploadUrl,
uploadAuthToken: getUploadUrl.data.authorizationToken,
fileName: storageKey,
data: Buffer.from(await file.arrayBuffer()),
});
if (upload.status !== 200) {
throw createFileNotFoundError();
}
return { storageKey };
},
getFileStream: async ({ storageKey }) => {
await b2Client.authorize();
const response = await b2Client.downloadFileByName({
bucketName,
fileName: storageKey,
responseType: 'stream',
});
if (!response.data) {
throw createFileNotFoundError();
}
return { fileStream: response.data };
},
deleteFile: async ({ storageKey }) => {
await b2Client.hideFile({
bucketId,
fileName: storageKey,
});
},
};
});

View File

@@ -9,6 +9,7 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
}
}

48
pnpm-lock.yaml generated
View File

@@ -250,6 +250,9 @@ importers:
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
backblaze-b2:
specifier: ^1.7.0
version: 1.7.0
better-auth:
specifier: 'catalog:'
version: 1.2.4(typescript@5.8.2)
@@ -320,6 +323,9 @@ importers:
'@total-typescript/ts-reset':
specifier: ^0.6.1
version: 0.6.1
'@types/backblaze-b2':
specifier: ^1.5.6
version: 1.5.6
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -2949,6 +2955,9 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
'@types/backblaze-b2@1.5.6':
resolution: {integrity: sha512-IGx7YhySgHYLns8nkDGPcejPpoG20eHdLBjtJK+QIh44OvG/QOBfCivfRajVoYKf2tczqCY/KIdOin6BfkF18Q==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -3578,6 +3587,12 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios-retry@3.9.1:
resolution: {integrity: sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==}
axios@0.21.4:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
@@ -3598,6 +3613,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
backblaze-b2@1.7.0:
resolution: {integrity: sha512-8cVsKkXspuM1UeLI8WWSWw2JHfB7/IvqTtzvwhHqqhNyqcYl8iZ2lFpeuXGKcFA1TiSRlgALXWFJ9eKG6+3ZPg==}
engines: {node: '>=10.0'}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -5165,6 +5184,10 @@ packages:
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-retry-allowed@2.2.0:
resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==}
engines: {node: '>=10'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
@@ -10192,6 +10215,10 @@ snapshots:
dependencies:
'@babel/types': 7.26.3
'@types/backblaze-b2@1.5.6':
dependencies:
'@types/node': 22.13.10
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@@ -11090,6 +11117,17 @@ snapshots:
asynckit@0.4.0: {}
axios-retry@3.9.1:
dependencies:
'@babel/runtime': 7.27.0
is-retry-allowed: 2.2.0
axios@0.21.4:
dependencies:
follow-redirects: 1.15.9
transitivePeerDependencies:
- debug
axios@1.8.4:
dependencies:
follow-redirects: 1.15.9
@@ -11117,6 +11155,14 @@ snapshots:
'@babel/core': 7.26.0
babel-plugin-jsx-dom-expressions: 0.39.3(@babel/core@7.26.0)
backblaze-b2@1.7.0:
dependencies:
axios: 0.21.4
axios-retry: 3.9.1
lodash: 4.17.21
transitivePeerDependencies:
- debug
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -13161,6 +13207,8 @@ snapshots:
is-potential-custom-element-name@1.0.1: {}
is-retry-allowed@2.2.0: {}
is-url@1.2.4: {}
is-what@4.1.16: {}