import { Injectable, Logger } from '@nestjs/common'; import { UploadedObjectInfo } from 'minio/dist/main/internal/type'; import { MinioService } from 'src/minio/minio.service'; @Injectable() export class KvService { private readonly kvBucketName = 'api.us.dev'; private readonly kvPrefix = 'kv'; private readonly kvMetadataFileName = 'metadata.json'; private readonly kvMetadataPath = `${this.kvPrefix}/${this.kvMetadataFileName}`; private readonly logger: Logger = new Logger(KvService.name); constructor(private readonly minioService: MinioService) {} public generateFilePath(namespace: string, key: string): string { return `${this.kvPrefix}/${namespace}/${key}`; } private async getMetadataFile(): Promise { return await this.minioService.getBuffer( this.kvBucketName, this.kvMetadataPath, ); } private async setMetadataFile(buffer: Buffer): Promise { await this.minioService.uploadBuffer( this.kvBucketName, this.kvMetadataPath, buffer, ); } private generateSecretKey(): string { return Math.random().toString(36).substring(2); } public async rotateSecretKey( namespace: string, secretKey: string, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey !== secretKey) { throw new Error('Secret key does not match'); } metadata[namespace].secretKey = this.generateSecretKey(); await this.setMetadataFile(Buffer.from(JSON.stringify(metadata))); this.logger.verbose(`Rotated secret key for ${namespace}`); return metadata[namespace]; } public async queryObjectMetadata( namespace: string, key: string, secretKey: string, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey !== secretKey) { throw new Error('Secret key does not match'); } const objectMetadata = await this.minioService.getObjectMetadata( this.kvBucketName, this.generateFilePath(namespace, key), ); return objectMetadata?.metaData; } public async get(namespace: string, key: string): Promise { const objectMetadata = await this.minioService.getObjectMetadata( this.kvBucketName, this.generateFilePath(namespace, key), ); if (objectMetadata?.metaData.public !== 'true') { throw new Error('Object is not public'); } return await this.minioService.getBuffer( this.kvBucketName, this.generateFilePath(namespace, key), ); } public async getWithSecretKey( namespace: string, key: string, secretKey: string, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey !== secretKey) { throw new Error('Secret key does not match'); } return await this.minioService.getBuffer( this.kvBucketName, this.generateFilePath(namespace, key), ); } public async claimNamespace( namespace: string, extraData: object = {}, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey) { throw new Error('Namespace already claimed'); } metadata[namespace] = { ...extraData, secretKey: this.generateSecretKey(), claimed: new Date().toISOString(), namespace, }; await this.setMetadataFile(Buffer.from(JSON.stringify(metadata))); this.logger.verbose(`Claimed namespace ${namespace}`); return metadata[namespace]; } public async set( namespace: string, key: string, value: Buffer, secretKey: string, options: { public: boolean; mimeType?: string; } = { public: false, }, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey !== secretKey) { throw new Error('Incorrect secret key for namespace'); } this.logger.verbose(`Setting ${namespace}/${key}`); const uploadResult = await this.minioService.uploadBuffer( this.kvBucketName, this.generateFilePath(namespace, key), Buffer.from(value), { set: new Date().toISOString(), namespace, ...options, }, ); return uploadResult; } public async delete(namespace: string, key: string): Promise { this.logger.verbose(`Deleting ${namespace}/${key}`); await this.minioService.deleteObject( this.kvBucketName, this.generateFilePath(namespace, key), ); } public async shareFile( namespace: string, key: string, secretKey: string, // Default expiry is 7 days expiry: number = 60 * 60 * 24 * 7, ): Promise { const metadata = await this.getMetadataFile().then((buffer) => JSON.parse(buffer.toString()), ); if (metadata[namespace]?.secretKey !== secretKey) { throw new Error('Incorrect secret key for namespace'); } const presignedUrl = await this.minioService.generatePresignedUrl( this.kvBucketName, this.generateFilePath(namespace, key), expiry, ); this.logger.verbose(`Generated presigned URL for ${namespace}/${key}`); return presignedUrl; } }