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 { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
||||||
import { AdsbExchangeModule } from './adsb-exchange/adsb-exchange.module';
|
import { AdsbExchangeModule } from './adsb-exchange/adsb-exchange.module';
|
||||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
import { JunkDrawerModule } from './junk-drawer/junk-drawer.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -100,6 +101,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
|
|||||||
FocoCoffeeModule,
|
FocoCoffeeModule,
|
||||||
JobsModule,
|
JobsModule,
|
||||||
AdsbExchangeModule,
|
AdsbExchangeModule,
|
||||||
|
JunkDrawerModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -40,4 +40,8 @@ export default () => ({
|
|||||||
apiKey: process.env.FOCO_LIVE_AIRTABLE_APIKEY ?? '',
|
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);
|
||||||
|
};
|
15
views/junk-drawer/upload-result.hbs
Normal file
15
views/junk-drawer/upload-result.hbs
Normal 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>
|
18
views/junk-drawer/upload.hbs
Normal file
18
views/junk-drawer/upload.hbs
Normal 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>
|
16
views/junk-drawer/view.hbs
Normal file
16
views/junk-drawer/view.hbs
Normal 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>
|
Reference in New Issue
Block a user