Add junk drawer

This commit is contained in:
2024-10-01 16:57:19 -06:00
parent 7fae07b3c3
commit e021be16a3
10 changed files with 311 additions and 0 deletions

View File

@@ -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: [

View File

@@ -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 ?? '',
},
});

View 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}` };
}
}

View 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 {}

View 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
View 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
View 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);
};

View File

@@ -0,0 +1,15 @@
<div>
<h1>Upload Result</h1>
<p>File uploaded successfully.</p>
<p>{{description}}</p>
<p>Right-click copy: <a href='/junk-drawer/{{slug}}'>{{slug}}</a></p>
{{#if items}}
<p>Files:</p>
<ul>
{{#each items}}
<li><a target="_blank" href='/junk-drawer/{{../slug}}/{{filename}}'>{{filename}}</a></li>
{{/each}}
</ul>
{{/if}}
<p><a href='/junk-drawer'>Upload another file</a></p>
</div>

View File

@@ -0,0 +1,18 @@
<form method='post' action='/junk-drawer/upload' enctype='multipart/form-data'>
<div>
<label for='files'>Choose files to upload</label>
<input type='file' id='files' name='files' multiple />
</div>
<div style='display:flex; flex-direction:column;'>
<label for='description'>Description</label>
<textarea
type='text'
id='description'
name='description'
rows='4'
></textarea>
</div>
<div>
<button>Submit</button>
</div>
</form>

View File

@@ -0,0 +1,16 @@
<div>
<h1>Junk Drawer Item</h1>
<p>The junk drawer item was uploaded {{lastModified}}</p>
<h2>Description</h2>
<p>{{description}}</p>
<p>Right-click copy: <a href='/junk-drawer/{{slug}}'>{{slug}}</a></p>
{{#if items}}
<p>Files:</p>
<ul>
{{#each items}}
<li><a href='/junk-drawer/{{../slug}}/{{filename}}'>{{filename}}</a></li>
{{/each}}
</ul>
{{/if}}
<p><a href='/junk-drawer'>Upload another file</a></p>
</div>