Skip to content

Commit 4c638f5

Browse files
committed
chore: Add FilestorageModule to CoreModule imports
1 parent 84c012f commit 4c638f5

File tree

8 files changed

+516
-1
lines changed

8 files changed

+516
-1
lines changed

src/core/core.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { JobsModule } from './jobs/jobs.module';
99
import { KeyringsModule } from './keyrings/keyrings.module';
1010
import { LoggerModule } from './logger/logger.module';
1111
import { TasksModule } from './tasks/tasks.module';
12+
import { FilestorageModule } from './filestorage/filestorage.module';
1213

1314
@Module({
14-
imports: [AuthModule, BackendsModule, LoggerModule, KeyringsModule, AgentsModule, JobsModule, TasksModule],
15+
imports: [AuthModule, BackendsModule, LoggerModule, KeyringsModule, AgentsModule, JobsModule, TasksModule, FilestorageModule],
1516
providers: [CoreService],
1617
controllers: [CoreController],
1718
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ApiProperty, PartialType } from '@nestjs/swagger'
2+
import { CustomFieldsDto } from '~/_common/abstracts/dto/custom-fields.dto'
3+
import { IsMongoId, IsString, IsEnum, IsOptional, IsObject, IsBoolean, IsNotEmpty, IsMimeType } from 'class-validator'
4+
import { FsType, FsTypeList } from '~/core/filestorage/_enum/fs-type.enum'
5+
import { MixedValue } from '~/_common/types/mixed-value.type'
6+
7+
export class FilestorageCreateDto extends CustomFieldsDto {
8+
@IsEnum(FsTypeList)
9+
@IsNotEmpty()
10+
@ApiProperty({ enum: FsTypeList })
11+
public type: FsType
12+
13+
@IsString()
14+
@IsNotEmpty()
15+
@ApiProperty()
16+
public namespace: string
17+
18+
@IsString()
19+
@IsNotEmpty()
20+
@ApiProperty()
21+
public path: string
22+
23+
@IsOptional()
24+
@IsMongoId()
25+
@ApiProperty({ type: String })
26+
public linkedTo?: string
27+
28+
@IsOptional()
29+
@IsString()
30+
@ApiProperty()
31+
public comments?: string
32+
33+
@IsOptional()
34+
@IsMimeType()
35+
@ApiProperty()
36+
public mime?: string
37+
38+
@IsOptional()
39+
@IsBoolean()
40+
@ApiProperty()
41+
public hidden?: boolean
42+
43+
@IsOptional()
44+
@IsObject()
45+
@ApiProperty({ type: Object })
46+
public tags?: { [key: string]: MixedValue }
47+
48+
@IsOptional()
49+
@IsObject()
50+
@ApiProperty({ type: Object })
51+
public acls?: { [key: string]: any } // eslint-disable-line
52+
}
53+
54+
export class FilestorageDto extends FilestorageCreateDto {
55+
@IsMongoId()
56+
@ApiProperty({ type: String })
57+
public _id: string
58+
}
59+
60+
export class FilestorageUpdateDto extends PartialType(FilestorageCreateDto) { }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export enum FsType {
2+
DIRECTORY = 'd',
3+
FILE = 'f',
4+
SYMLINK = 'l',
5+
}
6+
7+
export const FsTypeList: number[] = Object.keys(FsType)
8+
// eslint-disable-next-line
9+
.filter((k) => typeof FsType[k as any] === 'string')
10+
// eslint-disable-next-line
11+
.map((k) => FsType[k as any])
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2+
import { AbstractSchema } from '~/_common/abstracts/schemas/abstract.schema';
3+
import { FsType, FsTypeList } from '~/core/filestorage/_enum/fs-type.enum';
4+
5+
@Schema({
6+
collection: 'filestorage',
7+
versionKey: false,
8+
})
9+
export class Filestorage extends AbstractSchema {
10+
@Prop({
11+
required: true,
12+
type: String,
13+
enum: FsTypeList,
14+
})
15+
public type: FsType
16+
17+
@Prop({
18+
type: String,
19+
})
20+
public mime?: string
21+
22+
@Prop({
23+
required: true,
24+
type: String,
25+
})
26+
public namespace: string
27+
28+
@Prop({
29+
required: true,
30+
type: String,
31+
//TODO: check file path ..?
32+
})
33+
public path: string
34+
35+
@Prop({
36+
required: false,
37+
type: String,
38+
})
39+
public comments?: string
40+
41+
@Prop({
42+
type: Boolean,
43+
default: false,
44+
})
45+
public hidden: boolean
46+
47+
@Prop({
48+
required: false,
49+
type: Object,
50+
})
51+
public tags?: { [key: string]: any } // eslint-disable-line
52+
53+
@Prop({
54+
required: false,
55+
type: Object,
56+
})
57+
public acls?: { [key: string]: any } // eslint-disable-line
58+
59+
@Prop({
60+
required: false,
61+
type: Object,
62+
})
63+
// eslint-disable-next-line
64+
public customFields?: { [key: string]: any }
65+
}
66+
67+
export const FilestorageSchema = SchemaFactory.createForClass(Filestorage).index({ namespace: 1, path: 1 }, { unique: true })
68+
69+
FilestorageSchema.virtual('filename').get(function (this: Filestorage): string {
70+
return this.path.split('/').pop()
71+
})
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { AbstractService } from '~/_common/abstracts/abstract.service'
2+
import { Injectable } from '@nestjs/common'
3+
import { Response } from 'express'
4+
import { Filestorage } from '~/core/filestorage/_schemas/filestorage.schema'
5+
6+
@Injectable()
7+
export class TransformersFilestorageService extends AbstractService {
8+
public static readonly TRANSFORMERS = {
9+
'text/plain': TransformersFilestorageService.transformPlain,
10+
'text/html': TransformersFilestorageService.transformHtml,
11+
'application/pdf': TransformersFilestorageService.transformPdf,
12+
'image/*': TransformersFilestorageService.transformImage,
13+
}
14+
15+
public constructor() {
16+
super()
17+
}
18+
19+
public async transform(mime: string, res: Response, data: Filestorage, stream: NodeJS.ReadableStream, parent?: Filestorage): Promise<void> {
20+
const mimeType = mime || data.mime || 'application/octet-stream'
21+
const hasTransformer = TransformersFilestorageService.TRANSFORMERS.hasOwnProperty(mimeType)
22+
// TransformersFilestorageService.TRANSFORMERS.hasOwnProperty(mimeType) || TransformersFilestorageService.TRANSFORMERS.hasOwnProperty(mimeType.split('/')[0] + '/*')
23+
if (!hasTransformer) {
24+
if (TransformersFilestorageService.TRANSFORMERS.hasOwnProperty(mimeType.split('/')[0] + '/*')) {
25+
await TransformersFilestorageService.TRANSFORMERS[mimeType.split('/')[0] + '/*'](mime, res, data, stream)
26+
return
27+
}
28+
}
29+
if (!hasTransformer) {
30+
res.setHeader('Content-Type', mimeType)
31+
// eslint-disable-next-line
32+
res.setHeader('Content-Disposition', `attachment; filename="${(data as any).filename}"`)
33+
stream.pipe(res)
34+
return
35+
}
36+
await TransformersFilestorageService.TRANSFORMERS[mimeType](mime, res, data, stream)
37+
}
38+
39+
public static async transformPlain(_: string, res: Response, data: Filestorage, stream: NodeJS.ReadableStream): Promise<void> {
40+
res.setHeader('Content-Type', 'text/plain')
41+
// eslint-disable-next-line
42+
res.setHeader('Content-Disposition', `inline; filename="${(data as any).filename}"`)
43+
stream.pipe(res)
44+
return
45+
}
46+
47+
public static async transformPdf(_: string, res: Response, data: Filestorage, stream: NodeJS.ReadableStream): Promise<void> {
48+
res.setHeader('Content-Type', 'application/pdf')
49+
// eslint-disable-next-line
50+
res.setHeader('Content-Disposition', `inline; filename="${(data as any).filename}"`)
51+
stream.pipe(res)
52+
return
53+
}
54+
55+
public static async transformHtml(_: string, res: Response, data: Filestorage, stream: NodeJS.ReadableStream): Promise<void> {
56+
res.setHeader('Content-Type', 'text/html')
57+
// eslint-disable-next-line
58+
res.setHeader('Content-Disposition', `inline; filename="${(data as any).filename}"`)
59+
res.render('core/filestorage/transformers/html', {
60+
data,
61+
html: await TransformersFilestorageService.streamToString(stream),
62+
})
63+
}
64+
65+
public static async transformImage(mime: string, res: Response, data: Filestorage, stream: NodeJS.ReadableStream): Promise<void> {
66+
res.setHeader('Content-Type', 'image/' + mime.split('/').pop())
67+
// eslint-disable-next-line
68+
res.setHeader('Content-Disposition', `inline; filename="${(data as any).filename}"`)
69+
stream.pipe(res)
70+
return
71+
}
72+
73+
private static async streamToString(stream: NodeJS.ReadableStream): Promise<string> {
74+
const chunks = []
75+
return new Promise((resolve, reject) => {
76+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
77+
stream.on('error', (err) => reject(err))
78+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
79+
})
80+
}
81+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Body, Controller, Delete, Get, HttpStatus, Param, ParseFilePipe, Patch, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common'
2+
import { FileInterceptor } from '@nestjs/platform-express'
3+
import { ApiParam, ApiTags } from '@nestjs/swagger'
4+
import { FilterOptions, FilterSchema, ObjectIdValidationPipe, SearchFilterOptions, SearchFilterSchema } from '@the-software-compagny/nestjs_module_restools'
5+
import { Response } from 'express'
6+
import { Types } from 'mongoose'
7+
import { AbstractController } from '~/_common/abstracts/abstract.controller'
8+
import { ApiCreateDecorator } from '~/_common/decorators/api-create.decorator'
9+
import { ApiDeletedResponseDecorator } from '~/_common/decorators/api-deleted-response.decorator'
10+
import { ApiPaginatedDecorator } from '~/_common/decorators/api-paginated.decorator'
11+
import { ApiReadResponseDecorator } from '~/_common/decorators/api-read-response.decorator'
12+
import { ApiUpdateDecorator } from '~/_common/decorators/api-update.decorator'
13+
import { PickProjectionHelper } from '~/_common/helpers/pick-projection.helper'
14+
import { PartialProjectionType } from '~/_common/types/partial-projection.type'
15+
import { FilestorageCreateDto, FilestorageDto, FilestorageUpdateDto } from './_dto/filestorage.dto'
16+
import { TransformersFilestorageService } from './_services/transformers-filestorage.service'
17+
import { FilestorageService } from './filestorage.service'
18+
19+
@ApiTags('core')
20+
@Controller('filestorage')
21+
export class FilestorageController extends AbstractController {
22+
protected static readonly projection: PartialProjectionType<FilestorageDto> = {
23+
type: 1,
24+
namespace: 1,
25+
path: 1,
26+
hidden: 1,
27+
}
28+
29+
public constructor(private readonly _service: FilestorageService, private readonly transformerService: TransformersFilestorageService) {
30+
super()
31+
}
32+
33+
@Post()
34+
@UseInterceptors(FileInterceptor('file'))
35+
@ApiCreateDecorator(FilestorageCreateDto, FilestorageDto)
36+
public async create(
37+
@Res() res: Response,
38+
@Body() body: FilestorageCreateDto,
39+
@UploadedFile(new ParseFilePipe({ fileIsRequired: false })) file?: Express.Multer.File,
40+
): Promise<Response> {
41+
const data = await this._service.create({ ...body, file })
42+
return res.status(HttpStatus.CREATED).json({
43+
statusCode: HttpStatus.CREATED,
44+
data,
45+
})
46+
}
47+
48+
@Get()
49+
@ApiPaginatedDecorator(PickProjectionHelper(FilestorageDto, FilestorageController.projection))
50+
public async search(@Res() res: Response, @SearchFilterSchema() searchFilterSchema: FilterSchema, @SearchFilterOptions() searchFilterOptions: FilterOptions): Promise<Response> {
51+
const [data, total] = await this._service.findAndCount(searchFilterSchema, FilestorageController.projection, searchFilterOptions)
52+
return res.status(HttpStatus.OK).json({
53+
statusCode: HttpStatus.OK,
54+
total,
55+
data,
56+
})
57+
}
58+
59+
@Get(':_id([0-9a-fA-F]{24})')
60+
@ApiParam({ name: '_id', type: String })
61+
@ApiReadResponseDecorator(FilestorageDto)
62+
public async read(@Param('_id', ObjectIdValidationPipe) _id: Types.ObjectId, @Res() res: Response): Promise<Response> {
63+
const data = await this._service.findById(_id)
64+
return res.status(HttpStatus.OK).json({
65+
statusCode: HttpStatus.OK,
66+
data,
67+
})
68+
}
69+
70+
@Get('path')
71+
@ApiReadResponseDecorator(FilestorageDto)
72+
public async readPath(@Query('namespace') namespace: string, @Query('path') path: string, @Res() res: Response): Promise<Response> {
73+
const data = await this._service.findOne({ namespace, path })
74+
return res.status(HttpStatus.OK).json({
75+
statusCode: HttpStatus.OK,
76+
data,
77+
})
78+
}
79+
80+
@Get(':_id([0-9a-fA-F]{24})/raw')
81+
@ApiParam({ name: '_id', type: String })
82+
@ApiReadResponseDecorator(FilestorageDto)
83+
public async readRawData(@Res() res: Response, @Param('_id', ObjectIdValidationPipe) _id: Types.ObjectId, @Query('mime') mime: string = ''): Promise<void> {
84+
const [data, stream, parent] = await this._service.findByIdWithRawData(_id)
85+
await this.transformerService.transform(mime, res, data, stream, parent)
86+
}
87+
88+
@Get('path/raw')
89+
@ApiReadResponseDecorator(FilestorageDto)
90+
public async readPathRawData(@Res() res: Response, @Query('namespace') namespace: string, @Query('path') path: string, @Query('mime') mime: string = ''): Promise<void> {
91+
const [data, stream, parent] = await this._service.findOneWithRawData({ namespace, path })
92+
await this.transformerService.transform(mime, res, data, stream, parent)
93+
}
94+
95+
@Patch(':_id([0-9a-fA-F]{24})')
96+
@ApiParam({ name: '_id', type: String })
97+
@ApiUpdateDecorator(FilestorageUpdateDto, FilestorageDto)
98+
public async update(@Param('_id', ObjectIdValidationPipe) _id: Types.ObjectId, @Body() body: FilestorageUpdateDto, @Res() res: Response): Promise<Response> {
99+
const data = await this._service.update(_id, body)
100+
return res.status(HttpStatus.OK).json({
101+
statusCode: HttpStatus.OK,
102+
data,
103+
})
104+
}
105+
106+
@Delete(':_id([0-9a-fA-F]{24})')
107+
@ApiParam({ name: '_id', type: String })
108+
@ApiDeletedResponseDecorator(FilestorageDto)
109+
public async remove(@Param('_id', ObjectIdValidationPipe) _id: Types.ObjectId, @Res() res: Response): Promise<Response> {
110+
const data = await this._service.delete(_id)
111+
return res.status(HttpStatus.OK).json({
112+
statusCode: HttpStatus.OK,
113+
data,
114+
})
115+
}
116+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Module } from '@nestjs/common'
2+
import { MongooseModule } from '@nestjs/mongoose'
3+
import { Filestorage, FilestorageSchema } from './_schemas/filestorage.schema'
4+
import { FilestorageService } from './filestorage.service'
5+
import { FilestorageController } from './filestorage.controller'
6+
import { TransformersFilestorageService } from '~/core/filestorage/_services/transformers-filestorage.service'
7+
8+
@Module({
9+
imports: [
10+
MongooseModule.forFeatureAsync([
11+
{
12+
name: Filestorage.name,
13+
useFactory: () => FilestorageSchema,
14+
},
15+
]),
16+
],
17+
providers: [FilestorageService, TransformersFilestorageService],
18+
controllers: [FilestorageController],
19+
exports: [FilestorageService],
20+
})
21+
export class FilestorageModule { }

0 commit comments

Comments
 (0)