@avleon/core 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +601 -561
- package/package.json +38 -6
- package/src/application.ts +104 -125
- package/src/authentication.ts +16 -16
- package/src/cache.ts +91 -91
- package/src/collection.test.ts +71 -0
- package/src/collection.ts +344 -254
- package/src/config.test.ts +35 -0
- package/src/config.ts +85 -42
- package/src/constants.ts +1 -1
- package/src/container.ts +54 -54
- package/src/controller.ts +125 -127
- package/src/decorators.ts +27 -27
- package/src/environment-variables.ts +53 -46
- package/src/exceptions/http-exceptions.ts +86 -86
- package/src/exceptions/index.ts +1 -1
- package/src/exceptions/system-exception.ts +35 -34
- package/src/file-storage.ts +206 -206
- package/src/helpers.ts +324 -328
- package/src/icore.ts +66 -90
- package/src/index.ts +30 -30
- package/src/interfaces/avleon-application.ts +32 -40
- package/src/logger.ts +72 -72
- package/src/map-types.ts +159 -159
- package/src/middleware.ts +119 -98
- package/src/multipart.ts +116 -116
- package/src/openapi.ts +372 -372
- package/src/params.ts +111 -111
- package/src/queue.ts +126 -126
- package/src/response.ts +74 -74
- package/src/results.ts +30 -30
- package/src/route-methods.ts +186 -186
- package/src/swagger-schema.ts +213 -213
- package/src/testing.ts +220 -220
- package/src/types/app-builder.interface.ts +18 -19
- package/src/types/application.interface.ts +7 -9
- package/src/utils/hash.ts +8 -5
- package/src/utils/index.ts +2 -2
- package/src/utils/optional-require.ts +50 -50
- package/src/validation.ts +160 -156
- package/src/validator-extend.ts +25 -25
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2024
|
|
3
|
-
* @author Tareq Hossain
|
|
4
|
-
* @email xtrinsic96@gmail.com
|
|
5
|
-
* @url https://github.com/xtareq
|
|
6
|
-
*/
|
|
7
|
-
export abstract class BaseHttpException extends Error {
|
|
8
|
-
code: number = 500;
|
|
9
|
-
name: string = "HttpException";
|
|
10
|
-
constructor(message: any) {
|
|
11
|
-
super(JSON.stringify(message));
|
|
12
|
-
}
|
|
13
|
-
isCustomException() {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class BadRequestException extends BaseHttpException {
|
|
19
|
-
name: string = "BadRequest";
|
|
20
|
-
code: number = 400;
|
|
21
|
-
|
|
22
|
-
constructor(message: any) {
|
|
23
|
-
super(message);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class ValidationErrorException extends BadRequestException {
|
|
28
|
-
name: string = "ValidationError";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class InternalErrorException extends BaseHttpException {
|
|
32
|
-
name: string = "InternalError";
|
|
33
|
-
code: number = 500;
|
|
34
|
-
|
|
35
|
-
constructor(message: any = "Something going wrong") {
|
|
36
|
-
super(message);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class NotFoundException extends BaseHttpException {
|
|
41
|
-
name: string = "NotFound";
|
|
42
|
-
code: number = 404;
|
|
43
|
-
constructor(message: any) {
|
|
44
|
-
super(message);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class UnauthorizedException extends BaseHttpException {
|
|
49
|
-
name: string = "Unauthorized";
|
|
50
|
-
code: number = 401;
|
|
51
|
-
constructor(message: any) {
|
|
52
|
-
super(message);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class ForbiddenException extends BaseHttpException {
|
|
57
|
-
name: string = "Forbidden";
|
|
58
|
-
code: number = 403;
|
|
59
|
-
constructor(message: any) {
|
|
60
|
-
super(message);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type HttpExceptionTypes =
|
|
65
|
-
| NotFoundException
|
|
66
|
-
| BadRequestException
|
|
67
|
-
| UnauthorizedException
|
|
68
|
-
| InternalErrorException
|
|
69
|
-
| ForbiddenException;
|
|
70
|
-
|
|
71
|
-
// export type HttpExceptions = {
|
|
72
|
-
// NotFound: (message: any) => NotFoundException,
|
|
73
|
-
// ValidationError: (message: any) =>ValidationErrorException,
|
|
74
|
-
// BadRequest: (message: any) => BadRequestException,
|
|
75
|
-
// Unauthorized: (message: any) => UnauthorizedException,
|
|
76
|
-
// Forbidden: (message: any) => ForbiddenException,
|
|
77
|
-
// InternalError: (message: any) => InternalErrorException
|
|
78
|
-
// }
|
|
79
|
-
export const HttpExceptions = {
|
|
80
|
-
notFound:(message:any="")=>new NotFoundException(message),
|
|
81
|
-
validationError:(message:any="")=>new ValidationErrorException(message),
|
|
82
|
-
badRequest:(message:any="")=>new BadRequestException(message),
|
|
83
|
-
unauthorized:(message:any="")=>new UnauthorizedException(message),
|
|
84
|
-
forbidden:(message:any="")=>new ForbiddenException(message),
|
|
85
|
-
internalError: (message:any="")=> new InternalErrorException(message)
|
|
86
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2024
|
|
3
|
+
* @author Tareq Hossain
|
|
4
|
+
* @email xtrinsic96@gmail.com
|
|
5
|
+
* @url https://github.com/xtareq
|
|
6
|
+
*/
|
|
7
|
+
export abstract class BaseHttpException extends Error {
|
|
8
|
+
code: number = 500;
|
|
9
|
+
name: string = "HttpException";
|
|
10
|
+
constructor(message: any) {
|
|
11
|
+
super(JSON.stringify(message));
|
|
12
|
+
}
|
|
13
|
+
isCustomException() {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class BadRequestException extends BaseHttpException {
|
|
19
|
+
name: string = "BadRequest";
|
|
20
|
+
code: number = 400;
|
|
21
|
+
|
|
22
|
+
constructor(message: any) {
|
|
23
|
+
super(message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ValidationErrorException extends BadRequestException {
|
|
28
|
+
name: string = "ValidationError";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class InternalErrorException extends BaseHttpException {
|
|
32
|
+
name: string = "InternalError";
|
|
33
|
+
code: number = 500;
|
|
34
|
+
|
|
35
|
+
constructor(message: any = "Something going wrong") {
|
|
36
|
+
super(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class NotFoundException extends BaseHttpException {
|
|
41
|
+
name: string = "NotFound";
|
|
42
|
+
code: number = 404;
|
|
43
|
+
constructor(message: any) {
|
|
44
|
+
super(message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class UnauthorizedException extends BaseHttpException {
|
|
49
|
+
name: string = "Unauthorized";
|
|
50
|
+
code: number = 401;
|
|
51
|
+
constructor(message: any) {
|
|
52
|
+
super(message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class ForbiddenException extends BaseHttpException {
|
|
57
|
+
name: string = "Forbidden";
|
|
58
|
+
code: number = 403;
|
|
59
|
+
constructor(message: any) {
|
|
60
|
+
super(message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type HttpExceptionTypes =
|
|
65
|
+
| NotFoundException
|
|
66
|
+
| BadRequestException
|
|
67
|
+
| UnauthorizedException
|
|
68
|
+
| InternalErrorException
|
|
69
|
+
| ForbiddenException;
|
|
70
|
+
|
|
71
|
+
// export type HttpExceptions = {
|
|
72
|
+
// NotFound: (message: any) => NotFoundException,
|
|
73
|
+
// ValidationError: (message: any) =>ValidationErrorException,
|
|
74
|
+
// BadRequest: (message: any) => BadRequestException,
|
|
75
|
+
// Unauthorized: (message: any) => UnauthorizedException,
|
|
76
|
+
// Forbidden: (message: any) => ForbiddenException,
|
|
77
|
+
// InternalError: (message: any) => InternalErrorException
|
|
78
|
+
// }
|
|
79
|
+
export const HttpExceptions = {
|
|
80
|
+
notFound: (message: any = "") => new NotFoundException(message),
|
|
81
|
+
validationError: (message: any = "") => new ValidationErrorException(message),
|
|
82
|
+
badRequest: (message: any = "") => new BadRequestException(message),
|
|
83
|
+
unauthorized: (message: any = "") => new UnauthorizedException(message),
|
|
84
|
+
forbidden: (message: any = "") => new ForbiddenException(message),
|
|
85
|
+
internalError: (message: any = "") => new InternalErrorException(message),
|
|
86
|
+
};
|
package/src/exceptions/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./http-exceptions";
|
|
@@ -1,34 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2024
|
|
3
|
-
* @author Tareq Hossain
|
|
4
|
-
* @email xtrinsic96@gmail.com
|
|
5
|
-
* @url https://github.com/xtareq
|
|
6
|
-
*/
|
|
7
|
-
export interface IRouteDuplicateErr {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class SystemUseError extends Error {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
export class DuplicateRouteException extends Error {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2024
|
|
3
|
+
* @author Tareq Hossain
|
|
4
|
+
* @email xtrinsic96@gmail.com
|
|
5
|
+
* @url https://github.com/xtareq
|
|
6
|
+
*/
|
|
7
|
+
export interface IRouteDuplicateErr {
|
|
8
|
+
path: string;
|
|
9
|
+
mpath: string;
|
|
10
|
+
method: string;
|
|
11
|
+
controller: string;
|
|
12
|
+
inverseController?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SystemUseError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class DuplicateRouteException extends Error {
|
|
21
|
+
constructor(params: IRouteDuplicateErr) {
|
|
22
|
+
let sameController = params.controller == params.inverseController;
|
|
23
|
+
let message = `Duplicate route found for method ${params.method.toUpperCase()}:${params.path == "" ? "'/'" : params.path} `;
|
|
24
|
+
message += sameController
|
|
25
|
+
? `in ${params.controller}`
|
|
26
|
+
: `both in ${params.controller} and ${params.inverseController}`;
|
|
27
|
+
super(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class EnvironmentVariableNotFound extends Error {
|
|
32
|
+
constructor(key: string) {
|
|
33
|
+
super(`${key} not found in environment variables.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/file-storage.ts
CHANGED
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
import fs, { createReadStream, PathLike } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { pipeline } from "stream/promises";
|
|
4
|
-
import {
|
|
5
|
-
BadRequestException,
|
|
6
|
-
InternalErrorException,
|
|
7
|
-
} from "./exceptions/http-exceptions";
|
|
8
|
-
import { MultipartFile } from "./multipart";
|
|
9
|
-
import { AppService } from "./decorators";
|
|
10
|
-
import os from "os";
|
|
11
|
-
import { SystemUseError } from "./exceptions/system-exception";
|
|
12
|
-
import { SavedMultipartFile } from "@fastify/multipart";
|
|
13
|
-
|
|
14
|
-
interface TransformOptions {
|
|
15
|
-
resize?: { width: number; height: number };
|
|
16
|
-
format?: "jpeg" | "png" | "webp" | "avif";
|
|
17
|
-
quality?: number;
|
|
18
|
-
// Add other sharp options as needed
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/*
|
|
22
|
-
//temp file
|
|
23
|
-
files[0].type // "file"
|
|
24
|
-
files[0].filepath
|
|
25
|
-
files[0].fieldname
|
|
26
|
-
files[0].filename
|
|
27
|
-
files[0].encoding
|
|
28
|
-
files[0].mimetype
|
|
29
|
-
files[0].fields
|
|
30
|
-
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/*
|
|
34
|
-
// stream file
|
|
35
|
-
data.file // stream
|
|
36
|
-
data.fields // other parsed parts
|
|
37
|
-
data.fieldname
|
|
38
|
-
data.filename
|
|
39
|
-
data.encoding
|
|
40
|
-
data.mimetype
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
export interface FileStorageInterface {
|
|
44
|
-
transform(options: TransformOptions): FileStorage;
|
|
45
|
-
save(
|
|
46
|
-
file: MultipartFile,
|
|
47
|
-
options?: SaveOptionsSingle
|
|
48
|
-
): Promise<MultipartFile | undefined>;
|
|
49
|
-
saveAll(
|
|
50
|
-
files: MultipartFile[],
|
|
51
|
-
options?: SaveOptions
|
|
52
|
-
): Promise<MultipartFile[] | undefined>;
|
|
53
|
-
|
|
54
|
-
remove(filepath: PathLike): Promise<void>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SaveOptions {
|
|
58
|
-
overwrite?: boolean;
|
|
59
|
-
to?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface SaveOptionsSingle extends SaveOptions {
|
|
63
|
-
saveAs?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@AppService
|
|
67
|
-
export class FileStorage implements FileStorageInterface {
|
|
68
|
-
private transformOptions: TransformOptions | null = null;
|
|
69
|
-
|
|
70
|
-
transform(options: TransformOptions) {
|
|
71
|
-
this.transformOptions = options;
|
|
72
|
-
return this;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private isFileExists(fpath: PathLike) {
|
|
76
|
-
return fs.existsSync(fpath);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async save(f: MultipartFile, options?: SaveOptionsSingle) {
|
|
80
|
-
let foptions: SaveOptionsSingle = {
|
|
81
|
-
overwrite: options && options.overwrite ? options.overwrite : true,
|
|
82
|
-
};
|
|
83
|
-
try {
|
|
84
|
-
if (f.type == "file") {
|
|
85
|
-
const fname = path.join(process.cwd(), `public/${f.filename}`);
|
|
86
|
-
|
|
87
|
-
if (!foptions.overwrite && this.isFileExists(fname)) {
|
|
88
|
-
throw new SystemUseError("File already exits.");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
await pipeline(f.file, fs.createWriteStream(fname));
|
|
92
|
-
return f;
|
|
93
|
-
}
|
|
94
|
-
} catch (err) {
|
|
95
|
-
throw new SystemUseError("Can't upload file");
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async remove(filepath: PathLike) {
|
|
100
|
-
if (!this.isFileExists(path.join(process.cwd(), "public/" + filepath))) {
|
|
101
|
-
throw new SystemUseError("File doesn't exists.");
|
|
102
|
-
}
|
|
103
|
-
return fs.unlinkSync(path.join(process.cwd(), "public/" + filepath));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async saveAll(files: MultipartFile[], options?: SaveOptions) {
|
|
107
|
-
try {
|
|
108
|
-
let foptions: SaveOptions = {
|
|
109
|
-
overwrite: options && options.overwrite ? options.overwrite : true,
|
|
110
|
-
};
|
|
111
|
-
for (let f of files) {
|
|
112
|
-
let uploadPath =
|
|
113
|
-
if (options?.to) {
|
|
114
|
-
uploadPath = `public/${options.to}`;
|
|
115
|
-
}
|
|
116
|
-
const fname = path.join(process.cwd(), `${uploadPath}/${f.filename}`);
|
|
117
|
-
await this.ensureDirectoryExists(fname);
|
|
118
|
-
if (f.file) {
|
|
119
|
-
await pipeline(f.file, fs.createWriteStream(fname));
|
|
120
|
-
} else {
|
|
121
|
-
const fp = f as SavedMultipartFile;
|
|
122
|
-
await pipeline(
|
|
123
|
-
fs.createReadStream(fp.filepath),
|
|
124
|
-
fs.createWriteStream(fname)
|
|
125
|
-
);
|
|
126
|
-
fs.unlinkSync(fp.filepath);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return files;
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error(error);
|
|
132
|
-
throw new SystemUseError("Can't upload file");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private async processImage(
|
|
137
|
-
fileStream: NodeJS.ReadableStream,
|
|
138
|
-
outputPath: string
|
|
139
|
-
) {
|
|
140
|
-
try {
|
|
141
|
-
const sharp = await import("sharp"); // Lazy import sharp
|
|
142
|
-
|
|
143
|
-
let sharpPipeline = sharp.default();
|
|
144
|
-
|
|
145
|
-
if (this.transformOptions?.resize) {
|
|
146
|
-
sharpPipeline = sharpPipeline.resize(
|
|
147
|
-
this.transformOptions.resize.width,
|
|
148
|
-
this.transformOptions.resize.height
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (this.transformOptions?.format) {
|
|
153
|
-
switch (this.transformOptions.format) {
|
|
154
|
-
case "jpeg":
|
|
155
|
-
sharpPipeline = sharpPipeline.jpeg({
|
|
156
|
-
quality: this.transformOptions.quality || 80,
|
|
157
|
-
});
|
|
158
|
-
break;
|
|
159
|
-
case "png":
|
|
160
|
-
sharpPipeline = sharpPipeline.png({
|
|
161
|
-
quality: this.transformOptions.quality || 80,
|
|
162
|
-
});
|
|
163
|
-
break;
|
|
164
|
-
case "webp":
|
|
165
|
-
sharpPipeline = sharpPipeline.webp({
|
|
166
|
-
quality: this.transformOptions.quality || 80,
|
|
167
|
-
});
|
|
168
|
-
break;
|
|
169
|
-
case "avif":
|
|
170
|
-
sharpPipeline = sharpPipeline.avif({
|
|
171
|
-
quality: this.transformOptions.quality || 80,
|
|
172
|
-
});
|
|
173
|
-
break;
|
|
174
|
-
default:
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await pipeline(
|
|
180
|
-
fileStream,
|
|
181
|
-
sharpPipeline,
|
|
182
|
-
fs.createWriteStream(outputPath)
|
|
183
|
-
);
|
|
184
|
-
} catch (error: any) {
|
|
185
|
-
if (
|
|
186
|
-
error.code === "MODULE_NOT_FOUND" &&
|
|
187
|
-
error.message.includes("sharp")
|
|
188
|
-
) {
|
|
189
|
-
throw new InternalErrorException(
|
|
190
|
-
"sharp module not found. Please install sharp to use image transformations."
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
console.error("Image processing failed:", error);
|
|
194
|
-
throw new InternalErrorException("Image processing failed.");
|
|
195
|
-
} finally {
|
|
196
|
-
this.transformOptions = null; // Reset transform options after processing
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private async ensureDirectoryExists(filePath: string) {
|
|
201
|
-
const dir = path.dirname(filePath);
|
|
202
|
-
if (!fs.existsSync(dir)) {
|
|
203
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
1
|
+
import fs, { createReadStream, PathLike } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pipeline } from "stream/promises";
|
|
4
|
+
import {
|
|
5
|
+
BadRequestException,
|
|
6
|
+
InternalErrorException,
|
|
7
|
+
} from "./exceptions/http-exceptions";
|
|
8
|
+
import { MultipartFile } from "./multipart";
|
|
9
|
+
import { AppService } from "./decorators";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { SystemUseError } from "./exceptions/system-exception";
|
|
12
|
+
import { SavedMultipartFile } from "@fastify/multipart";
|
|
13
|
+
|
|
14
|
+
interface TransformOptions {
|
|
15
|
+
resize?: { width: number; height: number };
|
|
16
|
+
format?: "jpeg" | "png" | "webp" | "avif";
|
|
17
|
+
quality?: number;
|
|
18
|
+
// Add other sharp options as needed
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
//temp file
|
|
23
|
+
files[0].type // "file"
|
|
24
|
+
files[0].filepath
|
|
25
|
+
files[0].fieldname
|
|
26
|
+
files[0].filename
|
|
27
|
+
files[0].encoding
|
|
28
|
+
files[0].mimetype
|
|
29
|
+
files[0].fields
|
|
30
|
+
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
// stream file
|
|
35
|
+
data.file // stream
|
|
36
|
+
data.fields // other parsed parts
|
|
37
|
+
data.fieldname
|
|
38
|
+
data.filename
|
|
39
|
+
data.encoding
|
|
40
|
+
data.mimetype
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface FileStorageInterface {
|
|
44
|
+
transform(options: TransformOptions): FileStorage;
|
|
45
|
+
save(
|
|
46
|
+
file: MultipartFile,
|
|
47
|
+
options?: SaveOptionsSingle,
|
|
48
|
+
): Promise<MultipartFile | undefined>;
|
|
49
|
+
saveAll(
|
|
50
|
+
files: MultipartFile[],
|
|
51
|
+
options?: SaveOptions,
|
|
52
|
+
): Promise<MultipartFile[] | undefined>;
|
|
53
|
+
|
|
54
|
+
remove(filepath: PathLike): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SaveOptions {
|
|
58
|
+
overwrite?: boolean;
|
|
59
|
+
to?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SaveOptionsSingle extends SaveOptions {
|
|
63
|
+
saveAs?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@AppService
|
|
67
|
+
export class FileStorage implements FileStorageInterface {
|
|
68
|
+
private transformOptions: TransformOptions | null = null;
|
|
69
|
+
|
|
70
|
+
transform(options: TransformOptions) {
|
|
71
|
+
this.transformOptions = options;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private isFileExists(fpath: PathLike) {
|
|
76
|
+
return fs.existsSync(fpath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async save(f: MultipartFile, options?: SaveOptionsSingle) {
|
|
80
|
+
let foptions: SaveOptionsSingle = {
|
|
81
|
+
overwrite: options && options.overwrite ? options.overwrite : true,
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
if (f.type == "file") {
|
|
85
|
+
const fname = path.join(process.cwd(), `public/${f.filename}`);
|
|
86
|
+
|
|
87
|
+
if (!foptions.overwrite && this.isFileExists(fname)) {
|
|
88
|
+
throw new SystemUseError("File already exits.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await pipeline(f.file, fs.createWriteStream(fname));
|
|
92
|
+
return f;
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new SystemUseError("Can't upload file");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async remove(filepath: PathLike) {
|
|
100
|
+
if (!this.isFileExists(path.join(process.cwd(), "public/" + filepath))) {
|
|
101
|
+
throw new SystemUseError("File doesn't exists.");
|
|
102
|
+
}
|
|
103
|
+
return fs.unlinkSync(path.join(process.cwd(), "public/" + filepath));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async saveAll(files: MultipartFile[], options?: SaveOptions) {
|
|
107
|
+
try {
|
|
108
|
+
let foptions: SaveOptions = {
|
|
109
|
+
overwrite: options && options.overwrite ? options.overwrite : true,
|
|
110
|
+
};
|
|
111
|
+
for (let f of files) {
|
|
112
|
+
let uploadPath = "public";
|
|
113
|
+
if (options?.to) {
|
|
114
|
+
uploadPath = `public/${options.to}`;
|
|
115
|
+
}
|
|
116
|
+
const fname = path.join(process.cwd(), `${uploadPath}/${f.filename}`);
|
|
117
|
+
await this.ensureDirectoryExists(fname);
|
|
118
|
+
if (f.file) {
|
|
119
|
+
await pipeline(f.file, fs.createWriteStream(fname));
|
|
120
|
+
} else {
|
|
121
|
+
const fp = f as SavedMultipartFile;
|
|
122
|
+
await pipeline(
|
|
123
|
+
fs.createReadStream(fp.filepath),
|
|
124
|
+
fs.createWriteStream(fname),
|
|
125
|
+
);
|
|
126
|
+
fs.unlinkSync(fp.filepath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return files;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(error);
|
|
132
|
+
throw new SystemUseError("Can't upload file");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async processImage(
|
|
137
|
+
fileStream: NodeJS.ReadableStream,
|
|
138
|
+
outputPath: string,
|
|
139
|
+
) {
|
|
140
|
+
try {
|
|
141
|
+
const sharp = await import("sharp"); // Lazy import sharp
|
|
142
|
+
|
|
143
|
+
let sharpPipeline = sharp.default();
|
|
144
|
+
|
|
145
|
+
if (this.transformOptions?.resize) {
|
|
146
|
+
sharpPipeline = sharpPipeline.resize(
|
|
147
|
+
this.transformOptions.resize.width,
|
|
148
|
+
this.transformOptions.resize.height,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.transformOptions?.format) {
|
|
153
|
+
switch (this.transformOptions.format) {
|
|
154
|
+
case "jpeg":
|
|
155
|
+
sharpPipeline = sharpPipeline.jpeg({
|
|
156
|
+
quality: this.transformOptions.quality || 80,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
case "png":
|
|
160
|
+
sharpPipeline = sharpPipeline.png({
|
|
161
|
+
quality: this.transformOptions.quality || 80,
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
case "webp":
|
|
165
|
+
sharpPipeline = sharpPipeline.webp({
|
|
166
|
+
quality: this.transformOptions.quality || 80,
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
case "avif":
|
|
170
|
+
sharpPipeline = sharpPipeline.avif({
|
|
171
|
+
quality: this.transformOptions.quality || 80,
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await pipeline(
|
|
180
|
+
fileStream,
|
|
181
|
+
sharpPipeline,
|
|
182
|
+
fs.createWriteStream(outputPath),
|
|
183
|
+
);
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
if (
|
|
186
|
+
error.code === "MODULE_NOT_FOUND" &&
|
|
187
|
+
error.message.includes("sharp")
|
|
188
|
+
) {
|
|
189
|
+
throw new InternalErrorException(
|
|
190
|
+
"sharp module not found. Please install sharp to use image transformations.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
console.error("Image processing failed:", error);
|
|
194
|
+
throw new InternalErrorException("Image processing failed.");
|
|
195
|
+
} finally {
|
|
196
|
+
this.transformOptions = null; // Reset transform options after processing
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async ensureDirectoryExists(filePath: string) {
|
|
201
|
+
const dir = path.dirname(filePath);
|
|
202
|
+
if (!fs.existsSync(dir)) {
|
|
203
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|