/* * SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { BlobSASPermissions, BlobServiceClient, BlockBlobClient, ContainerClient, generateBlobSASQueryParameters, StorageSharedKeyCredential, } from '@azure/storage-blob'; import { MediaBackendType } from '@hedgedoc/commons'; import { Inject, Injectable } from '@nestjs/common'; import { FileTypeResult } from 'file-type'; import mediaConfiguration, { AzureMediaConfig, MediaConfig, } from '../../config/media.config'; import { MediaBackendError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; import { MediaBackend } from '../media-backend.interface'; @Injectable() export class AzureBackend implements MediaBackend { private config: AzureMediaConfig['azure']; private client: ContainerClient; private readonly credential: StorageSharedKeyCredential; constructor( private readonly logger: ConsoleLoggerService, @Inject(mediaConfiguration.KEY) private mediaConfig: MediaConfig, ) { this.logger.setContext(AzureBackend.name); // only create the backend if azure is configured if (this.mediaConfig.backend.use !== MediaBackendType.AZURE) { return; } this.config = this.mediaConfig.backend.azure; const blobServiceClient = BlobServiceClient.fromConnectionString( this.config.connectionString, ); this.credential = blobServiceClient.credential as StorageSharedKeyCredential; this.client = blobServiceClient.getContainerClient(this.config.container); } async saveFile( uuid: string, buffer: Buffer, fileType: FileTypeResult, ): Promise { const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid); try { await blockBlobClient.upload(buffer, buffer.length, { blobHTTPHeaders: { blobContentType: fileType.mime, }, }); this.logger.log(`Uploaded file ${uuid}`, 'saveFile'); return null; } catch (e) { this.logger.error( `error: ${(e as Error).message}`, (e as Error).stack, 'saveFile', ); throw new MediaBackendError(`Could not save file '${uuid}'`); } } async deleteFile(uuid: string, _: unknown): Promise { const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid); try { const response = await blockBlobClient.delete(); if (response.errorCode !== undefined) { throw new MediaBackendError( `Could not delete '${uuid}': ${response.errorCode}`, ); } this.logger.log(`Deleted file ${uuid}`, 'deleteFile'); } catch (e) { this.logger.error( `error: ${(e as Error).message}`, (e as Error).stack, 'deleteFile', ); throw new MediaBackendError(`Could not delete file ${uuid}`); } } getFileUrl(uuid: string, _: unknown): Promise { const blockBlobClient: BlockBlobClient = this.client.getBlockBlobClient(uuid); const blobSAS = generateBlobSASQueryParameters( { containerName: this.config.container, blobName: uuid, permissions: BlobSASPermissions.parse('r'), expiresOn: new Date(new Date().valueOf() + 3600 * 1000), }, this.credential, ); return Promise.resolve(`${blockBlobClient.url}?${blobSAS.toString()}`); } }