Compare commits

...

1 Commits

Author SHA1 Message Date
Corentin Thomasset
9ed9f34ee8 wip 2025-09-08 23:19:16 +02:00
15 changed files with 80 additions and 113 deletions

View File

@@ -18,7 +18,7 @@ const { config } = await parseConfig({ env });
await ensureLocalDatabaseDirectoryExists({ config }); await ensureLocalDatabaseDirectoryExists({ config });
const { db, client } = setupDatabase(config.database); const { db, client } = setupDatabase(config.database);
const documentsStorageService = createDocumentStorageService({ config }); const documentsStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
const taskServices = createTaskServices({ config }); const taskServices = createTaskServices({ config });
const { app } = await createServer({ config, db, taskServices, documentsStorageService }); const { app } = await createServer({ config, db, taskServices, documentsStorageService });

View File

@@ -26,7 +26,7 @@ async function createGlobalDependencies(partialDeps: Partial<GlobalDependencies>
const auth = partialDeps.auth ?? getAuth({ db, config, authEmailsServices: createAuthEmailsServices({ emailsServices }), trackingServices }).auth; const auth = partialDeps.auth ?? getAuth({ db, config, authEmailsServices: createAuthEmailsServices({ emailsServices }), trackingServices }).auth;
const subscriptionsServices = createSubscriptionsServices({ config }); const subscriptionsServices = createSubscriptionsServices({ config });
const taskServices = partialDeps.taskServices ?? createTaskServices({ config }); const taskServices = partialDeps.taskServices ?? createTaskServices({ config });
const documentsStorageService = partialDeps.documentsStorageService ?? createDocumentStorageService({ config }); const documentsStorageService = partialDeps.documentsStorageService ?? createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
return { return {
documentsStorageService, documentsStorageService,

View File

@@ -33,7 +33,7 @@ describe('documents usecases', () => {
organizationPlans: { isFreePlanUnlimited: true }, organizationPlans: { isFreePlanUnlimited: true },
documentsStorage: { driver: 'in-memory' }, documentsStorage: { driver: 'in-memory' },
}); });
const documentsStorageService = createDocumentStorageService({ config }); const documentsStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
const createDocument = await createDocumentCreationUsecase({ const createDocument = await createDocumentCreationUsecase({
db, db,
@@ -93,7 +93,7 @@ describe('documents usecases', () => {
documentsStorage: { driver: 'in-memory' }, documentsStorage: { driver: 'in-memory' },
}); });
const documentsStorageService = createDocumentStorageService({ config }); const documentsStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
let documentIdIndex = 1; let documentIdIndex = 1;
const createDocument = await createDocumentCreationUsecase({ const createDocument = await createDocumentCreationUsecase({
@@ -254,7 +254,7 @@ describe('documents usecases', () => {
}); });
const documentsRepository = createDocumentsRepository({ db }); const documentsRepository = createDocumentsRepository({ db });
const documentsStorageService = createDocumentStorageService({ config }); const documentsStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
const createDocument = await createDocumentCreationUsecase({ const createDocument = await createDocumentCreationUsecase({
documentsStorageService, documentsStorageService,
@@ -533,7 +533,7 @@ describe('documents usecases', () => {
}); });
const documentsRepository = createDocumentsRepository({ db }); const documentsRepository = createDocumentsRepository({ db });
const documentsStorageService = createDocumentStorageService({ config }); const documentsStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
const taggingRulesRepository = createTaggingRulesRepository({ db }); const taggingRulesRepository = createTaggingRulesRepository({ db });
const tagsRepository = createTagsRepository({ db }); const tagsRepository = createTagsRepository({ db });

View File

@@ -1,4 +1,5 @@
import type { Config } from '../../config/config.types'; import type { Config } from '../../config/config.types';
import type { DocumentStorageConfig } from './documents.storage.types';
import type { StorageDriver, StorageDriverFactory, StorageServices } from './drivers/drivers.models'; import type { StorageDriver, StorageDriverFactory, StorageServices } from './drivers/drivers.models';
import { createError } from '../../shared/errors/errors'; import { createError } from '../../shared/errors/errors';
import { isNil } from '../../shared/utils'; import { isNil } from '../../shared/utils';
@@ -17,8 +18,8 @@ const storageDriverFactories = {
export type DocumentStorageService = Awaited<ReturnType<typeof createDocumentStorageService>>; export type DocumentStorageService = Awaited<ReturnType<typeof createDocumentStorageService>>;
export function createDocumentStorageService({ config }: { config: Config }): StorageServices { export function createDocumentStorageService({ documentStorageConfig }: { documentStorageConfig: DocumentStorageConfig }): StorageServices {
const storageDriverName = config.documentsStorage.driver; const storageDriverName = documentStorageConfig.driver;
const storageDriverFactory: StorageDriverFactory | undefined = storageDriverFactories[storageDriverName]; const storageDriverFactory: StorageDriverFactory | undefined = storageDriverFactories[storageDriverName];
@@ -31,11 +32,11 @@ export function createDocumentStorageService({ config }: { config: Config }): St
}); });
} }
const storageDriver = storageDriverFactory({ config }); const storageDriver = storageDriverFactory({ documentStorageConfig });
return createDocumentStorageServiceFromDriver({ return createDocumentStorageServiceFromDriver({
storageDriver, storageDriver,
encryptionConfig: config.documentsStorage.encryption, encryptionConfig: documentStorageConfig.encryption,
}); });
} }

View File

@@ -0,0 +1,3 @@
import type { Config } from '../../config/config.types';
export type DocumentStorageConfig = Config['documentsStorage'];

View File

@@ -0,0 +1,12 @@
import { documentsTable } from "../documents.table";
import { DocumentStorageService } from "./documents.storage.services";
export async function migrateDocumentsStorage({db, inputDocumentStorageService, outputDocumentStorageService, logger = createLogger({ namespace: 'migrateDocumentsStorage' })}: {
db: Database;
inputDocumentStorageService: DocumentStorageService;
outputDocumentStorageService: DocumentStorageService;
logger?: Logger;
}) {
}

View File

@@ -1,3 +1,4 @@
import type { DocumentStorageConfig } from '../../documents.storage.types';
import { AzuriteContainer } from '@testcontainers/azurite'; import { AzuriteContainer } from '@testcontainers/azurite';
import { describe } from 'vitest'; import { describe } from 'vitest';
import { TEST_CONTAINER_IMAGES } from '../../../../../../test/containers/images'; import { TEST_CONTAINER_IMAGES } from '../../../../../../test/containers/images';
@@ -13,11 +14,7 @@ describe('az-blob storage-driver', () => {
const azuriteContainer = await new AzuriteContainer(TEST_CONTAINER_IMAGES.AZURITE).withInMemoryPersistence().start(); const azuriteContainer = await new AzuriteContainer(TEST_CONTAINER_IMAGES.AZURITE).withInMemoryPersistence().start();
const connectionString = azuriteContainer.getConnectionString(); const connectionString = azuriteContainer.getConnectionString();
const config = overrideConfig({ const driver = azBlobStorageDriverFactory({ documentStorageConfig: { drivers: { azureBlob: { connectionString, containerName: 'test-container' } } } as DocumentStorageConfig });
documentsStorage: { drivers: { azureBlob: { connectionString, containerName: 'test-container' } } },
});
const driver = azBlobStorageDriverFactory({ config });
const client = driver.getClient(); const client = driver.getClient();
await client.createContainer('test-container'); await client.createContainer('test-container');

View File

@@ -11,8 +11,8 @@ function isAzureBlobNotFoundError(error: Error): boolean {
return ('statusCode' in error && error.statusCode === 404) || ('code' in error && error.code === 'BlobNotFound'); return ('statusCode' in error && error.statusCode === 404) || ('code' in error && error.code === 'BlobNotFound');
} }
export const azBlobStorageDriverFactory = defineStorageDriver(({ config }) => { export const azBlobStorageDriverFactory = defineStorageDriver(({ documentStorageConfig }) => {
const { accountName, accountKey, containerName, connectionString } = config.documentsStorage.drivers.azureBlob; const { accountName, accountKey, containerName, connectionString } = documentStorageConfig.drivers.azureBlob;
const blobServiceClient = connectionString !== undefined const blobServiceClient = connectionString !== undefined
? BlobServiceClient.fromConnectionString(connectionString) ? BlobServiceClient.fromConnectionString(connectionString)

View File

@@ -1,6 +1,7 @@
import type { Readable } from 'node:stream'; import type { Readable } from 'node:stream';
import type { Config } from '../../../config/config.types'; import type { Config } from '../../../config/config.types';
import type { ExtendNamedArguments, ExtendReturnPromise } from '../../../shared/types'; import type { ExtendNamedArguments, ExtendReturnPromise } from '../../../shared/types';
import type { DocumentStorageConfig } from '../documents.storage.types';
export type StorageDriver = { export type StorageDriver = {
name: string; name: string;
@@ -31,7 +32,7 @@ export type StorageServices = {
deleteFile: StorageDriver['deleteFile']; deleteFile: StorageDriver['deleteFile'];
}; };
export type StorageDriverFactory = (args: { config: Config }) => StorageDriver; export type StorageDriverFactory = (args: { documentStorageConfig: DocumentStorageConfig }) => StorageDriver;
export function defineStorageDriver<T extends StorageDriverFactory>(factory: T) { export function defineStorageDriver<T extends StorageDriverFactory>(factory: T) {
return factory; return factory;

View File

@@ -1,4 +1,5 @@
import type { Config } from '../../../../config/config.types'; import type { Config } from '../../../../config/config.types';
import type { DocumentStorageConfig } from '../../documents.storage.types';
import fs from 'node:fs'; import fs from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path, { join } from 'node:path'; import path, { join } from 'node:path';
@@ -28,18 +29,10 @@ describe('storage driver', () => {
createDriver: async () => { createDriver: async () => {
const tmpDirectory = await createTmpDirectory(); const tmpDirectory = await createTmpDirectory();
const config = {
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
return { return {
driver: fsStorageDriverFactory({ config }), driver: fsStorageDriverFactory({
documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig,
}),
[Symbol.asyncDispose]: async () => { [Symbol.asyncDispose]: async () => {
await deleteTmpDirectory(tmpDirectory); await deleteTmpDirectory(tmpDirectory);
}, },
@@ -49,17 +42,7 @@ describe('storage driver', () => {
describe('saveFile', () => { describe('saveFile', () => {
test('persists the file to the filesystem', async () => { test('persists the file to the filesystem', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
const { storageKey } = await fsStorageDriver.saveFile({ const { storageKey } = await fsStorageDriver.saveFile({
fileStream: createReadableStream({ content: 'lorem ipsum' }), fileStream: createReadableStream({ content: 'lorem ipsum' }),
@@ -77,17 +60,7 @@ describe('storage driver', () => {
}); });
test('an error is raised if the file already exists', async () => { test('an error is raised if the file already exists', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
await fsStorageDriver.saveFile({ await fsStorageDriver.saveFile({
fileStream: createReadableStream({ content: 'lorem ipsum' }), fileStream: createReadableStream({ content: 'lorem ipsum' }),
@@ -109,17 +82,7 @@ describe('storage driver', () => {
describe('getFileStream', () => { describe('getFileStream', () => {
test('get a readable stream of a stored file', async () => { test('get a readable stream of a stored file', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
await fsStorageDriver.saveFile({ await fsStorageDriver.saveFile({
fileStream: createReadableStream({ content: 'lorem ipsum' }), fileStream: createReadableStream({ content: 'lorem ipsum' }),
@@ -139,17 +102,7 @@ describe('storage driver', () => {
}); });
test('an error is raised if the file does not exist', async () => { test('an error is raised if the file does not exist', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
await expect(fsStorageDriver.getFileStream({ storageKey: 'org_1/text-file.txt' })).rejects.toThrow(createFileNotFoundError()); await expect(fsStorageDriver.getFileStream({ storageKey: 'org_1/text-file.txt' })).rejects.toThrow(createFileNotFoundError());
}); });
@@ -157,17 +110,7 @@ describe('storage driver', () => {
describe('deleteFile', () => { describe('deleteFile', () => {
test('deletes a stored file', async () => { test('deletes a stored file', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
await fsStorageDriver.saveFile({ await fsStorageDriver.saveFile({
fileStream: createReadableStream({ content: 'lorem ipsum' }), fileStream: createReadableStream({ content: 'lorem ipsum' }),
@@ -189,17 +132,7 @@ describe('storage driver', () => {
}); });
test('when the file does not exist, an error is raised', async () => { test('when the file does not exist, an error is raised', async () => {
const config = { const fsStorageDriver = fsStorageDriverFactory({ documentStorageConfig: { drivers: { filesystem: { root: tmpDirectory } } } as DocumentStorageConfig });
documentsStorage: {
drivers: {
filesystem: {
root: tmpDirectory,
},
},
},
} as Config;
const fsStorageDriver = fsStorageDriverFactory({ config });
await expect(fsStorageDriver.deleteFile({ storageKey: 'org_1/text-file.txt' })).rejects.toThrow(createFileNotFoundError()); await expect(fsStorageDriver.deleteFile({ storageKey: 'org_1/text-file.txt' })).rejects.toThrow(createFileNotFoundError());
}); });

View File

@@ -8,8 +8,8 @@ import { createFileAlreadyExistsError } from './fs.storage-driver.errors';
export const FS_STORAGE_DRIVER_NAME = 'filesystem' as const; export const FS_STORAGE_DRIVER_NAME = 'filesystem' as const;
export const fsStorageDriverFactory = defineStorageDriver(({ config }) => { export const fsStorageDriverFactory = defineStorageDriver(({ documentStorageConfig }) => {
const { root } = config.documentsStorage.drivers.filesystem; const { root } = documentStorageConfig.drivers.filesystem;
const getStoragePath = ({ storageKey }: { storageKey: string }) => ({ storagePath: join(root, storageKey) }); const getStoragePath = ({ storageKey }: { storageKey: string }) => ({ storagePath: join(root, storageKey) });

View File

@@ -1,3 +1,4 @@
import type { DocumentStorageConfig } from '../../documents.storage.types';
import { CreateBucketCommand } from '@aws-sdk/client-s3'; import { CreateBucketCommand } from '@aws-sdk/client-s3';
import { LocalstackContainer } from '@testcontainers/localstack'; import { LocalstackContainer } from '@testcontainers/localstack';
import { describe } from 'vitest'; import { describe } from 'vitest';
@@ -16,8 +17,8 @@ describe('s3 storage-driver', () => {
const localstackContainer = await new LocalstackContainer(TEST_CONTAINER_IMAGES.LOCALSTACK).start(); const localstackContainer = await new LocalstackContainer(TEST_CONTAINER_IMAGES.LOCALSTACK).start();
const bucketName = 'test-bucket'; const bucketName = 'test-bucket';
const config = overrideConfig({ const driver = s3StorageDriverFactory({
documentsStorage: { documentStorageConfig: {
drivers: { drivers: {
s3: { s3: {
accessKeyId: 'test', accessKeyId: 'test',
@@ -28,11 +29,9 @@ describe('s3 storage-driver', () => {
forcePathStyle: true, forcePathStyle: true,
}, },
}, },
}, } as DocumentStorageConfig,
}); });
const driver = s3StorageDriverFactory({ config });
const s3Client = driver.getClient(); const s3Client = driver.getClient();
await s3Client.send(new CreateBucketCommand({ Bucket: bucketName })); await s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));

View File

@@ -15,8 +15,8 @@ function isS3NotFoundError(error: Error) {
|| ('Code' in error && typeof error.Code === 'string' && codes.includes(error.Code)); || ('Code' in error && typeof error.Code === 'string' && codes.includes(error.Code));
} }
export const s3StorageDriverFactory = defineStorageDriver(({ config }) => { export const s3StorageDriverFactory = defineStorageDriver(({ documentStorageConfig }) => {
const { accessKeyId, secretAccessKey, bucketName, region, endpoint, forcePathStyle } = config.documentsStorage.drivers.s3; const { accessKeyId, secretAccessKey, bucketName, region, endpoint, forcePathStyle } = documentStorageConfig.drivers.s3;
const s3Client = new S3Client({ const s3Client = new S3Client({
region, region,

View File

@@ -1,9 +1,14 @@
import type { Logger } from '@crowlog/logger'; import type { Logger } from '@crowlog/logger';
import type { Database } from '../../../app/database/database.types'; import type { Database } from '../../../app/database/database.types';
import type { Config } from '../../../config/config.types';
import type { DocumentStorageService } from '../documents.storage.services'; import type { DocumentStorageService } from '../documents.storage.services';
import { eq, isNull } from 'drizzle-orm'; import { eq, isNotNull, isNull } from 'drizzle-orm';
import { createLogger } from '../../../shared/logger/logger'; import { createLogger } from '../../../shared/logger/logger';
import { documentsTable } from '../../documents.table'; import { documentsTable } from '../../documents.table';
import {
createDocumentStorageService,
} from '../documents.storage.services';
export async function encryptAllUnencryptedDocuments({ export async function encryptAllUnencryptedDocuments({
db, db,
@@ -17,7 +22,12 @@ export async function encryptAllUnencryptedDocuments({
deleteUnencryptedAfterEncryption?: boolean; deleteUnencryptedAfterEncryption?: boolean;
}) { }) {
const documents = await db const documents = await db
.select({ id: documentsTable.id, originalStorageKey: documentsTable.originalStorageKey, fileName: documentsTable.originalName, mimeType: documentsTable.mimeType }) .select({
id: documentsTable.id,
originalStorageKey: documentsTable.originalStorageKey,
fileName: documentsTable.originalName,
mimeType: documentsTable.mimeType,
})
.from(documentsTable) .from(documentsTable)
.where(isNull(documentsTable.fileEncryptionKeyWrapped)) .where(isNull(documentsTable.fileEncryptionKeyWrapped))
.orderBy(documentsTable.id); .orderBy(documentsTable.id);
@@ -34,15 +44,26 @@ export async function encryptAllUnencryptedDocuments({
fileEncryptionKekVersion: null, fileEncryptionKekVersion: null,
}); });
const newStorageKey = `${originalStorageKey}.enc`; const newStorageKey = `${originalStorageKey}.enc`;
const { storageKey, ...encryptionFields } = await documentStorageService.saveFile({ fileStream, fileName, mimeType, storageKey: newStorageKey }); const { storageKey, ...encryptionFields }
= await documentStorageService.saveFile({
fileStream,
fileName,
mimeType,
storageKey: newStorageKey,
});
await db.update(documentsTable).set({ await db
.update(documentsTable)
.set({
...encryptionFields, ...encryptionFields,
originalStorageKey: storageKey, originalStorageKey: storageKey,
}).where(eq(documentsTable.id, id)); })
.where(eq(documentsTable.id, id));
if (deleteUnencryptedAfterEncryption) { if (deleteUnencryptedAfterEncryption) {
await documentStorageService.deleteFile({ storageKey: originalStorageKey }); await documentStorageService.deleteFile({
storageKey: originalStorageKey,
});
} }
} }
} }

View File

@@ -9,7 +9,7 @@ import { runScriptWithDb } from './commons/run-script';
await runScriptWithDb( await runScriptWithDb(
{ scriptName: 'encrypt-all-documents' }, { scriptName: 'encrypt-all-documents' },
async ({ db, config, logger, isDryRun }) => { async ({ db, config, logger, isDryRun }) => {
const documentStorageService = createDocumentStorageService({ config }); const documentStorageService = createDocumentStorageService({ documentStorageConfig: config.documentsStorage });
if (!config.documentsStorage.encryption.isEncryptionEnabled) { if (!config.documentsStorage.encryption.isEncryptionEnabled) {
logger.error('Document encryption is not enabled, skipping'); logger.error('Document encryption is not enabled, skipping');