186 lines
5.4 KiB
TypeScript
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;
|
|
}
|
|
}
|