Add file upload
This commit is contained in:
@@ -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],
|
||||
|
@@ -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()),
|
||||
}
|
||||
});
|
||||
|
36
src/file/file.controller.ts
Normal file
36
src/file/file.controller.ts
Normal 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
10
src/file/file.module.ts
Normal 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
73
src/file/file.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
8
views/file/upload-result.hbs
Normal file
8
views/file/upload-result.hbs
Normal 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
9
views/file/upload.hbs
Normal 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>
|
Reference in New Issue
Block a user