Add file upload

This commit is contained in:
2023-12-11 23:05:15 -07:00
parent 6b2fd89ab2
commit fbbfae4ab2
8 changed files with 149 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import { RedisClientOptions } from 'redis';
import { redisStore } from 'cache-manager-redis-yet';
import { RedirectsModule } from './redirects/redirects.module';
import { FileModule } from './file/file.module';
@Module({
imports: [
@@ -74,6 +75,7 @@ import { RedirectsModule } from './redirects/redirects.module';
MinioModule,
KvModule,
RedirectsModule,
FileModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -24,4 +24,9 @@ export default () => ({
password: process.env.REDIS_PASS ?? '',
db: parseInt(process.env.REDIS_DB ?? '1'),
},
file: {
bucketName: process.env.FILE_BUCKET_NAME ?? process.env.S3_BUCKET ?? 'api.us.dev-files',
// default file ttl in seconds
defaultTtl: parseInt(process.env.FILE_DEFAULT_TTL ?? (30 * 24 * 60 * 60).toString()),
}
});

View File

@@ -0,0 +1,36 @@
import { Controller, Get, Param, Redirect, Render, 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 } from '@nestjs/swagger';
@Controller('file')
export class FileController {
constructor(private readonly fileService: FileService) { }
@Get()
@Render('file/upload')
generateUploadForm() {
return {}
}
@Get(':key')
@Redirect('', 302)
async getFile(@Param('key') key: string) {
const url = await this.fileService.generatePresignedUrl(key)
return { url }
}
@Post()
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@Render('file/upload-result')
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(),
}
}
}

10
src/file/file.module.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FileService } from './file.service';
import { FileController } from './file.controller';
import { MinioService } from 'src/minio/minio.service';
@Module({
providers: [FileService, MinioService],
controllers: [FileController]
})
export class FileModule { }

73
src/file/file.service.ts Normal file
View File

@@ -0,0 +1,73 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { UploadedObjectInfo } from 'minio';
import { MinioService } from 'src/minio/minio.service';
@Injectable()
export class FileService {
private readonly logger = new Logger(FileService.name);
private readonly bucketName;
private readonly filePrefix = 'file';
private expirationTime: number;
constructor(
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');
try {
this.minioService.listBucketObjects(this.bucketName);
} catch (e) {
this.logger.log(`Error with bucket ${this.bucketName}: ${e.message}`);
}
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;
const key = this.generateRandomKey();
const uploadResult = await this.minioService.uploadBuffer(this.bucketName, [this.filePrefix, key].join('/'), file.buffer, {
expireAt,
originalFilename: file.originalname,
'content-type': file.mimetype,
});
return {
uploadResult,
key,
expireAt,
originalFilename: file.originalname,
}
}
@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 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);
}
}

View File

@@ -89,8 +89,9 @@ export class MinioService {
public async listBucketObjects(
bucketName: string,
prefix?: string,
recursive: boolean = false,
): Promise<string[]> {
const objectStream = await this.client.listObjects(bucketName, prefix);
const objectStream = await this.client.listObjectsV2(bucketName, prefix, recursive);
const objects = await new Promise<ItemBucketMetadata[]>(
(resolve, reject) => {
const objects: ItemBucketMetadata[] = [];
@@ -109,6 +110,10 @@ export class MinioService {
await this.client.removeObject(bucketName, objectName);
}
public async makeBucket(bucketName: string): Promise<void> {
return this.client.makeBucket(bucketName);
}
/**
*
* @param bucketName

View File

@@ -0,0 +1,8 @@
<div>
<h1>Upload Result</h1>
<p>File uploaded successfully.</p>
<p>File name: {{originalFilename}}</p>
<p>Right-click copy: <a href='/file/{{key}}'>{{key}}</a></p>
<p>Expires at {{expireTime}}</p>
<p><a href='/file'>Upload another file</a></p>
</div>

9
views/file/upload.hbs Normal file
View File

@@ -0,0 +1,9 @@
<form method='post' enctype='multipart/form-data'>
<div>
<label for='file'>Choose file to upload</label>
<input type='file' id='file' name='file' multiple />
</div>
<div>
<button>Submit</button>
</div>
</form>