Skip to content

Commit

Permalink
Merge pull request #42 from Rikthepixel/feature/blob-storage-integration
Browse files Browse the repository at this point in the history
Feature/blob storage integration
  • Loading branch information
Rikthepixel authored Jan 10, 2024
2 parents b9a307d + 08454d3 commit 84375fe
Show file tree
Hide file tree
Showing 6 changed files with 16,197 additions and 15,840 deletions.
26 changes: 23 additions & 3 deletions apps/advertisements/src/providers/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Knex from 'knex';
import ConsoleLogger from '../loggers/ConsoleLogger';
import IoCContainer from 'tioc';
import { createBrokerAsPromised } from 'rascal';
import { BlobServiceClient } from '@azure/storage-blob';
import dbConfig from '../configs/db';
import authConfig from '../configs/auth';
import brokerConfig from '../configs/broker';
Expand All @@ -19,10 +20,20 @@ import FileImageRepository from '../repositories/images/FileImageRepository';
import { mkdirSync } from 'fs';
import path from 'path';
import ImageService from '../services/ImageService';
import AzureImageRepository from '../repositories/images/AzureImageRepository';

const depenencyProvider = (c: IoCContainer) =>
c
.addSingleton('db', () => Knex(dbConfig))
.addScoped('blobStorage', () => {
const connectionString = process.env.STORAGE_AZURE_CONNECTION_STRING;
if (!connectionString) {
throw Error(
"STORAGE_DRIVER='azure' was selected, but no STORAGE_AZURE_CONNECTION_STRING was specified",
);
}
return BlobServiceClient.fromConnectionString(connectionString);
})
.addSingleton(
'broker',
async () => await createBrokerAsPromised(brokerConfig),
Expand Down Expand Up @@ -50,9 +61,18 @@ const depenencyProvider = (c: IoCContainer) =>
(c) => new KnexCategoryPropertyOptionValueRepository(c.resolve('db')),
)
.addScoped('ImageRepository', (c) => {
const basePath = path.join(process.cwd(), 'storage', 'images');
mkdirSync(basePath, { recursive: true });
return new FileImageRepository(c.resolve('db'), basePath);
if (process.env.STORAGE_DRIVER === 'azure') {
return new AzureImageRepository(
c.resolve('db'),
c.resolve('blobStorage').getContainerClient('advertisement-images'),
);
} else if (process.env.STORAGE_DRIVER === 'file') {
const basePath = path.join(process.cwd(), 'storage', 'images');
mkdirSync(basePath, { recursive: true });
return new FileImageRepository(c.resolve('db'), basePath);
} else {
throw new Error('Invalid STORAGE_DRIVER');
}
})
.addScoped(
'UserService',
Expand Down
135 changes: 135 additions & 0 deletions apps/advertisements/src/repositories/images/AzureImageRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Knex } from 'knex';
import { ContainerClient } from '@azure/storage-blob';

import { UidOrId, castUidOrId, getType, isId } from '../../helpers/identifiers';
import ImageRepository, { UploadableImage } from './ImageRepository';
import { Readable } from 'stream';

export default class AzureImageRepository implements ImageRepository {
constructor(
private db: Knex,
private blobStorage: ContainerClient,
) {}

async upload(
advertisementId: number,
images: UploadableImage[],
): Promise<void> {
this.db.transaction(async (trx) => {
await Promise.all([
trx.table('images').insert(
images.map((img) => ({
uid: trx.fn.uuidToBin(img.uid),
advertisement_id: advertisementId,
mime: img.mime,
})),
),
...images.map(async (img) => {
return this.blobStorage
.getBlockBlobClient(img.uid)
.uploadData(img.content);
}),
]);
});
}

async getByAdvertisement(advertisementId: number): Promise<string[]> {
return await this.db
.table('images')
.where('advertisement_id', advertisementId)
.select<{ uid: Buffer }[]>('images.uid')
.then((imgs) => imgs.map((img) => this.db.fn.binToUuid(img.uid)));
}

async deleteByAdvertisement(advertisementId: number): Promise<void> {
await this.db.transaction(async (trx) => {
const images = await trx
.table('images')
.where('advertisement_id', advertisementId)
.select<{ uid: Buffer; id: number }[]>('uid', 'id');

if (!images || images.length === 0) return;

await trx
.table('images')
.whereIn(
'id',
images.map((img) => img.id),
)
.del();

await Promise.all(
images.map(async (img) => {
return this.blobStorage
.getBlockBlobClient(trx.fn.binToUuid(img.uid))
.deleteIfExists();
}),
);
});
}

async deleteMultiple(imagesToDelete: UidOrId[]): Promise<void> {
await this.db.transaction(async (trx) => {
const imageUidsToDelete: Buffer[] = [];
const imageIdsToDelete: number[] = [];

for (const image of imagesToDelete) {
if (isId(image)) {
imageIdsToDelete.push(image);
continue;
}
imageUidsToDelete.push(trx.fn.uuidToBin(image));
}

const images = await trx
.table('images')
.whereIn('images.uid', imageUidsToDelete)
.orWhereIn('images.id', imageUidsToDelete)
.select<{ id: number; uid: Buffer }[]>('uid', 'id');

if (!images || images.length === 0) return;

await trx
.table('images')
.whereIn(
'id',
images.map((img) => img.id),
)
.del();

await Promise.all(
images.map(async (img) => {
return this.blobStorage
.getBlockBlobClient(trx.fn.binToUuid(img.uid))
.deleteIfExists();
}),
);
});
}

async getContent(uidOrId: UidOrId) {
const image = await this.db
.table('images')
.where(
`images.${getType(uidOrId)}`,
castUidOrId(uidOrId, this.db.fn.uuidToBin),
)
.select('images.mime', 'images.uid')
.first<{ mime: string; uid: Buffer } | null>();

if (!image) return null;

const blobClient = this.blobStorage.getBlockBlobClient(
this.db.fn.binToUuid(image.uid),
);

if (!(await blobClient.exists())) return null;

return {
content: await blobClient
.download()
.then((res) => new Readable().wrap(res.readableStreamBody!)),
mime: image.mime,
};
}
}
Loading

0 comments on commit 84375fe

Please sign in to comment.