Add proxy with file rewrite

This commit is contained in:
2024-06-26 16:51:03 -06:00
parent e0f7c29244
commit c25a5230d9
3 changed files with 199 additions and 100 deletions

View File

@@ -1,10 +1,20 @@
import { Controller, Get, Param, Redirect, Render, UploadedFile, UseInterceptors } from '@nestjs/common';
import {
Controller,
Get,
Param,
Redirect,
Render,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileService } from './file.service';
import { Post } from '@nestjs/common';
import { UploadedObjectInfo } from 'minio';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Response } from 'express';
@Controller('file')
@ApiTags('file')
@@ -14,44 +24,67 @@ export class FileController {
@Get()
@Render('file/upload')
generateUploadForm() {
return {}
return {};
}
@Get(':key/direct')
@Redirect('', 302)
async getFile(@Param('key') key: string) {
const url = await this.fileService.generatePresignedUrl(key);
return { url };
}
@Get(':key')
@Redirect('', 302)
async getFile(@Param('key') key: string) {
const url = await this.fileService.generatePresignedUrl(key)
return { url }
async getFileWithProxy(@Param('key') key: string, @Res() res: Response) {
if (!(await this.fileService.fileExists(key))) {
return res.status(404).send('File not found or expired');
}
const buffer = await this.fileService.getFileFromS3(key);
const metadata = await this.fileService.getFileMetadataFromS3(key);
const { originalfilename, 'content-type': contentType } = metadata.metaData;
return res
.header('Content-Type', contentType)
.header(
'Content-Disposition',
`attachment; filename="${originalfilename ?? key}"`,
)
.send(buffer);
}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@Render('file/upload-result')
async handleFileUpload(@UploadedFile() file: Express.Multer.File): Promise<any> {
async handleFileUpload(
@UploadedFile() file: Express.Multer.File,
): Promise<any> {
const upload = await this.fileService.handleFileUpload(file);
return {
...upload,
expireTime: new Date(upload.expireAt * 1000).toLocaleString(),
}
};
}
@Post('upload.txt')
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
async handleFileSilentUpload(@UploadedFile() file: Express.Multer.File): Promise<string> {
async handleFileSilentUpload(
@UploadedFile() file: Express.Multer.File,
): Promise<string> {
const upload = await this.fileService.handleFileUpload(file);
return `https://api.us.dev/file/${upload.key}`
return `https://api.us.dev/file/${upload.key}`;
}
@Post('upload.json')
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
async handleFileUploadJson(@UploadedFile() file: Express.Multer.File): Promise<any> {
async handleFileUploadJson(
@UploadedFile() file: Express.Multer.File,
): Promise<any> {
const upload = await this.fileService.handleFileUpload(file);
return {
...upload,
expireTime: new Date(upload.expireAt * 1000).toLocaleString(),
}
};
}
}

View File

@@ -1,10 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { UploadedObjectInfo } from 'minio';
import { ItemBucketMetadata, UploadedObjectInfo } from 'minio';
import { MinioService } from 'src/minio/minio.service';
@Injectable()
export class FileService {
private readonly logger = new Logger(FileService.name);
@@ -16,44 +15,89 @@ export class FileService {
private readonly minioService: MinioService,
private readonly configService: ConfigService,
) {
this.expirationTime = this.configService.get('file.defaultTtl', 30 * 24 * 60 * 60);
this.bucketName = this.configService.get('file.bucketName', 'api.us.dev-files');
this.expirationTime = this.configService.get(
'file.defaultTtl',
30 * 24 * 60 * 60,
);
this.bucketName = this.configService.get(
'file.bucketName',
'api.us.dev-files',
);
try {
this.minioService.listBucketObjects(this.bucketName);
} catch (e) {
this.logger.log(`Error with bucket ${this.bucketName}: ${e.message}`);
}
this.deleteExpiredFiles()
this.deleteExpiredFiles();
}
private generateRandomKey(): string {
return Math.random().toString(36).substring(2);
}
async handleFileUpload(file: Express.Multer.File): Promise<{ uploadResult: UploadedObjectInfo, expireAt: number, key: string, originalFilename: string }> {
const expireAt = (Date.now() / 1000) + this.expirationTime;
private pathFromKey(key: string): string {
return [this.filePrefix, key].join('/');
}
async handleFileUpload(file: Express.Multer.File): Promise<{
uploadResult: UploadedObjectInfo;
expireAt: number;
key: string;
originalFilename: string;
}> {
const expireAt = Date.now() / 1000 + this.expirationTime;
const key = this.generateRandomKey();
const uploadResult = await this.minioService.uploadBuffer(this.bucketName, [this.filePrefix, key].join('/'), file.buffer, {
const uploadResult = await this.minioService.uploadBuffer(
this.bucketName,
this.pathFromKey(key),
file.buffer,
{
expireAt,
originalFilename: file.originalname,
'content-type': file.mimetype,
});
},
);
return {
uploadResult,
key,
expireAt,
originalFilename: file.originalname,
};
}
async getFileFromS3(key: string): Promise<Buffer> {
return this.minioService.getBuffer(this.bucketName, this.pathFromKey(key));
}
async getFileMetadataFromS3(key: string): Promise<ItemBucketMetadata> {
return this.minioService.getObjectMetadata(
this.bucketName,
this.pathFromKey(key),
);
}
async fileExists(key: string): Promise<boolean> {
return this.minioService.objectExists(
this.bucketName,
this.pathFromKey(key),
);
}
@Cron('0 0 * * *')
private async deleteExpiredFiles(): Promise<void> {
this.logger.debug('Running cron job to delete expired files');
const now = Date.now() / 1000;
const objectNames = await this.minioService.listBucketObjects(this.bucketName, this.filePrefix, true);
const objectNames = await this.minioService.listBucketObjects(
this.bucketName,
this.filePrefix,
true,
);
for (const objectName of objectNames) {
this.logger.debug(`Checking object ${objectName}`)
const objectInfo = await this.minioService.getObjectMetadata(this.bucketName, objectName);
this.logger.debug(`Checking object ${objectName}`);
const objectInfo = await this.minioService.getObjectMetadata(
this.bucketName,
objectName,
);
if (objectInfo.metaData.expireat < now) {
this.logger.debug(`Deleting object ${objectName}`);
await this.minioService.deleteObject(this.bucketName, objectName);
@@ -63,11 +107,17 @@ export class FileService {
async generatePresignedUrl(key: string): Promise<string> {
const objectPath = [this.filePrefix, key].join('/');
const metadata = await this.minioService.getObjectMetadata(this.bucketName, objectPath);
const metadata = await this.minioService.getObjectMetadata(
this.bucketName,
objectPath,
);
if (metadata.expireAt < Date.now() / 1000) {
throw new Error('Object has expired');
}
return await this.minioService.generatePresignedUrl(this.bucketName, objectPath, 10);
return await this.minioService.generatePresignedUrl(
this.bucketName,
objectPath,
10,
);
}
}

View File

@@ -91,7 +91,11 @@ export class MinioService {
prefix?: string,
recursive: boolean = false,
): Promise<string[]> {
const objectStream = await this.client.listObjectsV2(bucketName, prefix, recursive);
const objectStream = await this.client.listObjectsV2(
bucketName,
prefix,
recursive,
);
const objects = await new Promise<ItemBucketMetadata[]>(
(resolve, reject) => {
const objects: ItemBucketMetadata[] = [];
@@ -128,4 +132,16 @@ export class MinioService {
): Promise<string> {
return await this.client.presignedGetObject(bucketName, objectName, expiry);
}
public async objectExists(
bucketName: string,
objectName: string,
): Promise<boolean> {
try {
await this.client.statObject(bucketName, objectName);
return true;
} catch (e) {
return false;
}
}
}