Files
us-api/src/kv/kv.service.ts

186 lines
5.4 KiB
TypeScript

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<Buffer> {
return await this.minioService.getBuffer(
this.kvBucketName,
this.kvMetadataPath,
);
}
private async setMetadataFile(buffer: Buffer): Promise<void> {
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<object> {
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<object> {
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<Buffer> {
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<Buffer> {
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<void> {
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<UploadedObjectInfo> {
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<void> {
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<string> {
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;
}
}