Add junk drawer
This commit is contained in:
@@ -27,6 +27,7 @@ import { JobsModule } from './jobs/jobs.module';
|
||||
import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
||||
import { AdsbExchangeModule } from './adsb-exchange/adsb-exchange.module';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -100,6 +101,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
FocoCoffeeModule,
|
||||
JobsModule,
|
||||
AdsbExchangeModule,
|
||||
JunkDrawerModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
@@ -40,4 +40,8 @@ export default () => ({
|
||||
apiKey: process.env.FOCO_LIVE_AIRTABLE_APIKEY ?? '',
|
||||
},
|
||||
},
|
||||
junkDrawer: {
|
||||
bucketName: process.env.JUNK_DRAWER_BUCKET_NAME ?? 'junk-drawer',
|
||||
rootPath: process.env.JUNK_DRAWER_ROOT_PATH ?? '',
|
||||
},
|
||||
});
|
||||
|
94
src/junk-drawer/junk-drawer.controller.ts
Normal file
94
src/junk-drawer/junk-drawer.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Redirect,
|
||||
Render,
|
||||
Res,
|
||||
UploadedFile,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { JunkDrawerService } from './junk-drawer.service';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { generateUniqueSlug } from 'src/utils/slug';
|
||||
import { JunkDrawerMetadata } from './types';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller('junk-drawer')
|
||||
@ApiTags('junk-drawer')
|
||||
export class JunkDrawerController {
|
||||
constructor(private readonly junkDrawerService: JunkDrawerService) {}
|
||||
|
||||
@Get('')
|
||||
@Render('junk-drawer/upload')
|
||||
generateUploadForm() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@Get(':slug')
|
||||
@Render('junk-drawer/view')
|
||||
async viewJunkDrawer(@Param('slug') slug: string): Promise<any> {
|
||||
const metadata = await this.junkDrawerService.getJunkDrawerMetadata(slug);
|
||||
return { ...metadata };
|
||||
}
|
||||
|
||||
@Get(':slug/:filename')
|
||||
async downloadJunkDrawerItem(
|
||||
@Param('slug') slug: string,
|
||||
@Param('filename') filename: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const metadata = await this.junkDrawerService.getJunkDrawerMetadata(slug);
|
||||
if (!metadata) {
|
||||
return res.status(404).send('File not found or expired');
|
||||
}
|
||||
const item = metadata.items.find((item) => item.filename === filename);
|
||||
if (!item) {
|
||||
return res.status(404).send('File not found or expired');
|
||||
}
|
||||
const buffer = await this.junkDrawerService.getJunkDrawerItem(
|
||||
slug,
|
||||
filename,
|
||||
);
|
||||
return res
|
||||
.header('Content-Type', item.mimetype)
|
||||
.header('Content-Disposition', `filename="${item.filename}"`)
|
||||
.send(buffer);
|
||||
}
|
||||
|
||||
@Post('upload')
|
||||
@Redirect('', 302)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@UseInterceptors(FilesInterceptor('files'))
|
||||
async handleFileUpload(
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@Body('description') description: string,
|
||||
): Promise<any> {
|
||||
const uniqueSlug = generateUniqueSlug();
|
||||
const metadata: JunkDrawerMetadata = {
|
||||
slug: uniqueSlug,
|
||||
version: '1.0.0',
|
||||
lastModified: new Date(),
|
||||
description,
|
||||
items: files.map((file) => ({
|
||||
filename: file.originalname,
|
||||
size: file.size,
|
||||
lastModified: new Date(),
|
||||
mimetype: file.mimetype,
|
||||
})),
|
||||
};
|
||||
for (const file of files) {
|
||||
await this.junkDrawerService.storeJunkDrawerItem(
|
||||
uniqueSlug,
|
||||
file.originalname,
|
||||
file.buffer,
|
||||
);
|
||||
}
|
||||
await this.junkDrawerService.storeJunkDrawerMetadata(metadata);
|
||||
return { url: `/junk-drawer/${uniqueSlug}` };
|
||||
}
|
||||
}
|
10
src/junk-drawer/junk-drawer.module.ts
Normal file
10
src/junk-drawer/junk-drawer.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JunkDrawerService } from './junk-drawer.service';
|
||||
import { JunkDrawerController } from './junk-drawer.controller';
|
||||
import { MinioService } from 'src/minio/minio.service';
|
||||
|
||||
@Module({
|
||||
providers: [JunkDrawerService, MinioService],
|
||||
controllers: [JunkDrawerController],
|
||||
})
|
||||
export class JunkDrawerModule {}
|
95
src/junk-drawer/junk-drawer.service.ts
Normal file
95
src/junk-drawer/junk-drawer.service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MinioService } from 'src/minio/minio.service';
|
||||
import { JunkDrawerMetadata } from './types';
|
||||
import { generateUniqueSlug } from 'src/utils/slug';
|
||||
|
||||
@Injectable()
|
||||
export class JunkDrawerService {
|
||||
constructor(
|
||||
private readonly minioService: MinioService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
private pathForFile(filename: string): string {
|
||||
return [this.configService.get<string>('junkDrawer.rootPath'), filename]
|
||||
.join('/')
|
||||
.replace(/\.\.\//g, '');
|
||||
}
|
||||
|
||||
private junkDrawerBucketName(): string {
|
||||
return this.configService.get<string>(
|
||||
'junkDrawer.bucketName',
|
||||
'junk-drawer',
|
||||
);
|
||||
}
|
||||
|
||||
public generateSlug(fileName: string): string {
|
||||
return (
|
||||
generateUniqueSlug() +
|
||||
fileName
|
||||
.replace(/[^a-z0-9]/gi, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
);
|
||||
}
|
||||
|
||||
public async getJunkDrawerMetadata(
|
||||
slug: string,
|
||||
): Promise<JunkDrawerMetadata | undefined> {
|
||||
const metadata = await this.minioService.getBuffer(
|
||||
this.junkDrawerBucketName(),
|
||||
this.pathForFile(`${slug}/metadata.json`),
|
||||
);
|
||||
if (metadata) {
|
||||
return JSON.parse(metadata.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public async storeJunkDrawerMetadata(
|
||||
metadata: JunkDrawerMetadata,
|
||||
): Promise<JunkDrawerMetadata | undefined> {
|
||||
const uploadResult = await this.minioService.uploadBuffer(
|
||||
this.junkDrawerBucketName(),
|
||||
this.pathForFile(`${metadata.slug}/metadata.json`),
|
||||
Buffer.from(JSON.stringify(metadata)),
|
||||
);
|
||||
if (uploadResult) {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
public async storeJunkDrawerItem(
|
||||
slug: string,
|
||||
filename: string,
|
||||
buffer: Buffer,
|
||||
): Promise<void> {
|
||||
const uploadResult = this.minioService.uploadBuffer(
|
||||
this.junkDrawerBucketName(),
|
||||
this.pathForFile(`${slug}/${filename}`),
|
||||
buffer,
|
||||
);
|
||||
}
|
||||
|
||||
public async storeJunkDrawerCollection(
|
||||
metadata: JunkDrawerMetadata,
|
||||
files: { filename: string; buffer: Buffer }[],
|
||||
): Promise<void> {
|
||||
await this.storeJunkDrawerMetadata(metadata);
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
this.storeJunkDrawerItem(metadata.slug, file.filename, file.buffer),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public async getJunkDrawerItem(
|
||||
slug: string,
|
||||
filename: string,
|
||||
): Promise<Buffer | undefined> {
|
||||
return this.minioService.getBuffer(
|
||||
this.junkDrawerBucketName(),
|
||||
this.pathForFile(`${slug}/${filename}`),
|
||||
);
|
||||
}
|
||||
}
|
14
src/junk-drawer/types.ts
Normal file
14
src/junk-drawer/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface JunkDrawerMetadata {
|
||||
slug: string;
|
||||
version: string;
|
||||
lastModified: Date;
|
||||
description: string;
|
||||
items: JunkDrawerItem[];
|
||||
}
|
||||
|
||||
export interface JunkDrawerItem {
|
||||
filename: string;
|
||||
size: number;
|
||||
lastModified: Date;
|
||||
mimetype: string;
|
||||
}
|
43
src/utils/slug.ts
Normal file
43
src/utils/slug.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
interface SlugOptions {
|
||||
year: boolean;
|
||||
month: boolean;
|
||||
day: boolean;
|
||||
hour: boolean;
|
||||
minute: boolean;
|
||||
second: boolean;
|
||||
random: boolean;
|
||||
separator: string;
|
||||
}
|
||||
|
||||
const defaultSlugOptions: SlugOptions = {
|
||||
year: true,
|
||||
month: true,
|
||||
day: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
second: false,
|
||||
random: false,
|
||||
separator: '',
|
||||
};
|
||||
|
||||
export const generateUniqueSlug = (
|
||||
options: Partial<SlugOptions> = {},
|
||||
): string => {
|
||||
const { year, month, day, hour, minute, second, random, separator } = {
|
||||
...defaultSlugOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
const parts = [
|
||||
year ? date.getFullYear() : '',
|
||||
month ? String(date.getMonth() + 1).padStart(2, '0') : '',
|
||||
day ? String(date.getDate()).padStart(2, '0') : '',
|
||||
hour ? String(date.getHours()).padStart(2, '0') : '',
|
||||
minute ? String(date.getMinutes()).padStart(2, '0') : '',
|
||||
second ? String(date.getSeconds()).padStart(2, '0') : '',
|
||||
random ? Math.random().toString(36).substring(2, 15) : '',
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.join(separator);
|
||||
};
|
Reference in New Issue
Block a user