Add proxy with file rewrite
This commit is contained in:
@@ -1,57 +1,90 @@
|
|||||||
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 { FileService } from './file.service';
|
||||||
import { Post } from '@nestjs/common';
|
import { Post } from '@nestjs/common';
|
||||||
import { UploadedObjectInfo } from 'minio';
|
import { UploadedObjectInfo } from 'minio';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
@Controller('file')
|
@Controller('file')
|
||||||
@ApiTags('file')
|
@ApiTags('file')
|
||||||
export class FileController {
|
export class FileController {
|
||||||
constructor(private readonly fileService: FileService) { }
|
constructor(private readonly fileService: FileService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Render('file/upload')
|
@Render('file/upload')
|
||||||
generateUploadForm() {
|
generateUploadForm() {
|
||||||
return {}
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':key')
|
@Get(':key/direct')
|
||||||
@Redirect('', 302)
|
@Redirect('', 302)
|
||||||
async getFile(@Param('key') key: string) {
|
async getFile(@Param('key') key: string) {
|
||||||
const url = await this.fileService.generatePresignedUrl(key)
|
const url = await this.fileService.generatePresignedUrl(key);
|
||||||
return { url }
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('upload')
|
@Get(':key')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
async getFileWithProxy(@Param('key') key: string, @Res() res: Response) {
|
||||||
@ApiConsumes('multipart/form-data')
|
if (!(await this.fileService.fileExists(key))) {
|
||||||
@Render('file/upload-result')
|
return res.status(404).send('File not found or expired');
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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.txt')
|
@Post('upload')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
async handleFileSilentUpload(@UploadedFile() file: Express.Multer.File): Promise<string> {
|
@Render('file/upload-result')
|
||||||
const upload = await this.fileService.handleFileUpload(file);
|
async handleFileUpload(
|
||||||
return `https://api.us.dev/file/${upload.key}`
|
@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.json')
|
@Post('upload.txt')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
async handleFileUploadJson(@UploadedFile() file: Express.Multer.File): Promise<any> {
|
async handleFileSilentUpload(
|
||||||
const upload = await this.fileService.handleFileUpload(file);
|
@UploadedFile() file: Express.Multer.File,
|
||||||
return {
|
): Promise<string> {
|
||||||
...upload,
|
const upload = await this.fileService.handleFileUpload(file);
|
||||||
expireTime: new Date(upload.expireAt * 1000).toLocaleString(),
|
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> {
|
||||||
|
const upload = await this.fileService.handleFileUpload(file);
|
||||||
|
return {
|
||||||
|
...upload,
|
||||||
|
expireTime: new Date(upload.expireAt * 1000).toLocaleString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,73 +1,123 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { UploadedObjectInfo } from 'minio';
|
import { ItemBucketMetadata, UploadedObjectInfo } from 'minio';
|
||||||
import { MinioService } from 'src/minio/minio.service';
|
import { MinioService } from 'src/minio/minio.service';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
private readonly logger = new Logger(FileService.name);
|
private readonly logger = new Logger(FileService.name);
|
||||||
private readonly bucketName;
|
private readonly bucketName;
|
||||||
private readonly filePrefix = 'file';
|
private readonly filePrefix = 'file';
|
||||||
private expirationTime: number;
|
private expirationTime: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly minioService: MinioService,
|
private readonly minioService: MinioService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.expirationTime = this.configService.get('file.defaultTtl', 30 * 24 * 60 * 60);
|
this.expirationTime = this.configService.get(
|
||||||
this.bucketName = this.configService.get('file.bucketName', 'api.us.dev-files');
|
'file.defaultTtl',
|
||||||
try {
|
30 * 24 * 60 * 60,
|
||||||
this.minioService.listBucketObjects(this.bucketName);
|
);
|
||||||
} catch (e) {
|
this.bucketName = this.configService.get(
|
||||||
this.logger.log(`Error with bucket ${this.bucketName}: ${e.message}`);
|
'file.bucketName',
|
||||||
}
|
'api.us.dev-files',
|
||||||
this.deleteExpiredFiles()
|
);
|
||||||
|
try {
|
||||||
|
this.minioService.listBucketObjects(this.bucketName);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.log(`Error with bucket ${this.bucketName}: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
this.deleteExpiredFiles();
|
||||||
|
}
|
||||||
|
|
||||||
private generateRandomKey(): string {
|
private generateRandomKey(): string {
|
||||||
return Math.random().toString(36).substring(2);
|
return Math.random().toString(36).substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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,
|
||||||
|
);
|
||||||
|
for (const objectName of objectNames) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleFileUpload(file: Express.Multer.File): Promise<{ uploadResult: UploadedObjectInfo, expireAt: number, key: string, originalFilename: string }> {
|
async generatePresignedUrl(key: string): Promise<string> {
|
||||||
const expireAt = (Date.now() / 1000) + this.expirationTime;
|
const objectPath = [this.filePrefix, key].join('/');
|
||||||
const key = this.generateRandomKey();
|
const metadata = await this.minioService.getObjectMetadata(
|
||||||
const uploadResult = await this.minioService.uploadBuffer(this.bucketName, [this.filePrefix, key].join('/'), file.buffer, {
|
this.bucketName,
|
||||||
expireAt,
|
objectPath,
|
||||||
originalFilename: file.originalname,
|
);
|
||||||
'content-type': file.mimetype,
|
if (metadata.expireAt < Date.now() / 1000) {
|
||||||
});
|
throw new Error('Object has expired');
|
||||||
return {
|
|
||||||
uploadResult,
|
|
||||||
key,
|
|
||||||
expireAt,
|
|
||||||
originalFilename: file.originalname,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return await this.minioService.generatePresignedUrl(
|
||||||
@Cron('0 0 * * *')
|
this.bucketName,
|
||||||
private async deleteExpiredFiles(): Promise<void> {
|
objectPath,
|
||||||
this.logger.debug('Running cron job to delete expired files');
|
10,
|
||||||
const now = Date.now() / 1000;
|
);
|
||||||
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);
|
|
||||||
if (objectInfo.metaData.expireat < now) {
|
|
||||||
this.logger.debug(`Deleting object ${objectName}`);
|
|
||||||
await this.minioService.deleteObject(this.bucketName, objectName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generatePresignedUrl(key: string): Promise<string> {
|
|
||||||
const objectPath = [this.filePrefix, key].join('/');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -91,7 +91,11 @@ export class MinioService {
|
|||||||
prefix?: string,
|
prefix?: string,
|
||||||
recursive: boolean = false,
|
recursive: boolean = false,
|
||||||
): Promise<string[]> {
|
): 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[]>(
|
const objects = await new Promise<ItemBucketMetadata[]>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const objects: ItemBucketMetadata[] = [];
|
const objects: ItemBucketMetadata[] = [];
|
||||||
@@ -128,4 +132,16 @@ export class MinioService {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return await this.client.presignedGetObject(bucketName, objectName, expiry);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user