@drax/media-back 3.10.0 → 3.12.0
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/dist/controllers/MediaController.js +17 -83
- package/dist/index.js +2 -1
- package/dist/services/MediaService.js +123 -0
- package/env.d.ts +10 -0
- package/package.json +3 -3
- package/src/controllers/MediaController.ts +17 -97
- package/src/index.ts +15 -1
- package/src/services/MediaService.ts +198 -0
- package/test/{modules/media/File → endpoints}/File-endpoints.test.ts +4 -4
- package/test/endpoints/Media-endpoints.test.ts +124 -0
- package/test/service/Media-service.test.ts +112 -0
- package/test/setup/TestSetup.ts +11 -0
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/types/controllers/MediaController.d.ts +2 -1
- package/types/controllers/MediaController.d.ts.map +1 -1
- package/types/index.d.ts +4 -2
- package/types/index.d.ts.map +1 -1
- package/types/schemas/FileSchema.d.ts +1 -1
- package/types/services/MediaService.d.ts +56 -0
- package/types/services/MediaService.d.ts.map +1 -0
- package/test/store/087fd8919e269ac2-test.jpg +0 -0
- package/test/store/134ce2d244520699-test.jpg +0 -0
- package/test/store/1cf908a6a99e5dfc-test.jpg +0 -0
- package/test/store/41f86768454e8d13-test.jpg +0 -0
- package/test/store/578e5f99ca2db751-test.jpg +0 -0
- package/test/store/71a6d03abe415748-test.jpg +0 -0
- package/test/store/9d73f482a69e78c0-test.jpg +0 -0
- package/test/store/ddd197b164e9b35f-test.jpg +0 -0
- package/test/store/test.jpg +0 -0
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import { CommonController,
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { CommonController, } from "@drax/common-back";
|
|
3
2
|
import { MediaPermissions } from "../permissions/MediaPermissions.js";
|
|
4
|
-
import {
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
7
|
-
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : '';
|
|
3
|
+
import { MediaService } from "../services/MediaService.js";
|
|
8
4
|
class MediaController extends CommonController {
|
|
9
5
|
constructor() {
|
|
10
6
|
super();
|
|
11
|
-
|
|
12
|
-
validateDir(dir) {
|
|
13
|
-
let dirRegExp = /^[a-zA-Z0-9_-]+$/;
|
|
14
|
-
if (!dir || dirRegExp.test(dir) === false) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
return true;
|
|
7
|
+
this.mediaService = new MediaService();
|
|
18
8
|
}
|
|
19
9
|
async uploadFile(request, reply) {
|
|
20
10
|
try {
|
|
@@ -24,56 +14,23 @@ class MediaController extends CommonController {
|
|
|
24
14
|
username: request.rbac.username,
|
|
25
15
|
};
|
|
26
16
|
const dir = request.params.dir;
|
|
27
|
-
if (!this.validateDir(dir)) {
|
|
28
|
-
reply.statusCode = 400;
|
|
29
|
-
reply.send({ error: 'Invalid directory name' });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
17
|
const data = await request.file();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const relativePath = storedFile.path;
|
|
44
|
-
const absolutePath = path.resolve(process.cwd(), relativePath);
|
|
45
|
-
const extension = StoreManager.getExtension(storedFile.filename);
|
|
46
|
-
const fileService = FileServiceFactory.instance;
|
|
47
|
-
const FILE_METADATA = process.env.DRAX_FILE_METADATA ? (/true|yes|enable/i).test(process.env.DRAX_FILE_METADATA) : true;
|
|
48
|
-
if (FILE_METADATA === true) {
|
|
49
|
-
try {
|
|
50
|
-
await fileService.registerUploadedFile({
|
|
51
|
-
filename: storedFile.filename,
|
|
52
|
-
relativePath: relativePath,
|
|
53
|
-
absolutePath: absolutePath,
|
|
54
|
-
size: storedFile.size,
|
|
55
|
-
mimetype: storedFile.mimetype || data.mimetype,
|
|
56
|
-
encoding: storedFile.encoding || data.encoding || '',
|
|
57
|
-
extension,
|
|
58
|
-
type: storedFile.mimetype?.split('/')[0] || '',
|
|
59
|
-
lastAccess: new Date(),
|
|
60
|
-
ttlSeconds: 0,
|
|
61
|
-
hits: 0,
|
|
62
|
-
url: urlFile,
|
|
63
|
-
createdBy,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (e) {
|
|
67
|
-
await StoreManager.deleteFile(destinationPath, storedFile.filename).catch(() => undefined);
|
|
68
|
-
throw e;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
18
|
+
const storedFile = await this.mediaService.saveFile({
|
|
19
|
+
dir,
|
|
20
|
+
file: {
|
|
21
|
+
filename: data.filename,
|
|
22
|
+
fileStream: data.file,
|
|
23
|
+
mimetype: data.mimetype,
|
|
24
|
+
encoding: data.encoding,
|
|
25
|
+
},
|
|
26
|
+
createdBy,
|
|
27
|
+
});
|
|
71
28
|
let theFile = {
|
|
72
29
|
filename: storedFile.filename,
|
|
73
|
-
filepath: storedFile.
|
|
30
|
+
filepath: storedFile.relativePath,
|
|
74
31
|
size: storedFile.size,
|
|
75
32
|
mimetype: storedFile.mimetype,
|
|
76
|
-
url:
|
|
33
|
+
url: storedFile.url,
|
|
77
34
|
};
|
|
78
35
|
return theFile;
|
|
79
36
|
}
|
|
@@ -87,31 +44,8 @@ class MediaController extends CommonController {
|
|
|
87
44
|
const year = request.params.year;
|
|
88
45
|
const month = request.params.month;
|
|
89
46
|
const filename = request.params.filename;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
reply.statusCode = 400;
|
|
93
|
-
reply.send({ error: 'Invalid directory name' });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (/[0-9]{4}/.test(year) == false) {
|
|
97
|
-
reply.statusCode = 400;
|
|
98
|
-
reply.send({ error: 'Invalid year' });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (/[0-9]{2}/.test(month) == false) {
|
|
102
|
-
reply.statusCode = 400;
|
|
103
|
-
reply.send({ error: 'Invalid month' });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const fileDir = join(BASE_FILE_DIR, dir, year, month);
|
|
107
|
-
//console.log("FILE_DIR: ", fileDir, " FILENAME:", filename)
|
|
108
|
-
//Agregar hit al archivo
|
|
109
|
-
const FILE_METADATA = process.env.DRAX_FILE_METADATA ? (/true|yes|enable/i).test(process.env.DRAX_FILE_METADATA) : true;
|
|
110
|
-
if (FILE_METADATA === true) {
|
|
111
|
-
const fileService = FileServiceFactory.instance;
|
|
112
|
-
await fileService.registerDownloadHit(join(fileDir, filename));
|
|
113
|
-
}
|
|
114
|
-
return reply.sendFile(filename, fileDir);
|
|
47
|
+
const file = await this.mediaService.getFile({ dir, year, month, filename });
|
|
48
|
+
return reply.sendFile(file.filename, file.fileDir);
|
|
115
49
|
}
|
|
116
50
|
catch (e) {
|
|
117
51
|
this.handleError(e, reply);
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import FileModel from "./models/FileModel.js";
|
|
|
7
7
|
import FileMongoRepository from "./repository/mongo/FileMongoRepository.js";
|
|
8
8
|
import FileSqliteRepository from "./repository/sqlite/FileSqliteRepository.js";
|
|
9
9
|
import FileService from "./services/FileService.js";
|
|
10
|
+
import MediaService from "./services/MediaService.js";
|
|
10
11
|
import FileServiceFactory from "./factory/services/FileServiceFactory.js";
|
|
11
12
|
import FileController from "./controllers/FileController.js";
|
|
12
13
|
export {
|
|
@@ -15,4 +16,4 @@ MediaRoutes, FileRoutes,
|
|
|
15
16
|
//Permissions
|
|
16
17
|
MediaPermissions, FilePermissions,
|
|
17
18
|
//File
|
|
18
|
-
FileSchema, FileModel, FileMongoRepository, FileSqliteRepository, FileService, FileServiceFactory, FileController };
|
|
19
|
+
FileSchema, FileModel, FileMongoRepository, FileSqliteRepository, FileService, MediaService, FileServiceFactory, FileController };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BadRequestError, CommonConfig, DraxConfig, NotFoundError, StoreManager, } from "@drax/common-back";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { FileServiceFactory } from "../factory/services/FileServiceFactory.js";
|
|
5
|
+
class MediaService {
|
|
6
|
+
getBaseFileDir() {
|
|
7
|
+
return DraxConfig.getOrLoad(CommonConfig.FileDir) || "files";
|
|
8
|
+
}
|
|
9
|
+
getBaseUrl() {
|
|
10
|
+
return DraxConfig.getOrLoad(CommonConfig.BaseUrl)
|
|
11
|
+
? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, "")
|
|
12
|
+
: "";
|
|
13
|
+
}
|
|
14
|
+
validateDir(dir) {
|
|
15
|
+
const dirRegExp = /^[a-zA-Z0-9_-]+$/;
|
|
16
|
+
return !!dir && dirRegExp.test(dir);
|
|
17
|
+
}
|
|
18
|
+
validateYear(year) {
|
|
19
|
+
return /^[0-9]{4}$/.test(year);
|
|
20
|
+
}
|
|
21
|
+
validateMonth(month) {
|
|
22
|
+
return /^[0-9]{2}$/.test(month);
|
|
23
|
+
}
|
|
24
|
+
assertDir(dir) {
|
|
25
|
+
if (!this.validateDir(dir)) {
|
|
26
|
+
throw new BadRequestError("Invalid directory name");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
assertYear(year) {
|
|
30
|
+
if (!this.validateYear(year)) {
|
|
31
|
+
throw new BadRequestError("Invalid year");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
assertMonth(month) {
|
|
35
|
+
if (!this.validateMonth(month)) {
|
|
36
|
+
throw new BadRequestError("Invalid month");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
isMetadataEnabled() {
|
|
40
|
+
return process.env.DRAX_FILE_METADATA
|
|
41
|
+
? /true|yes|enable/i.test(process.env.DRAX_FILE_METADATA)
|
|
42
|
+
: true;
|
|
43
|
+
}
|
|
44
|
+
buildDatePathParts(date) {
|
|
45
|
+
return {
|
|
46
|
+
year: date.getFullYear().toString(),
|
|
47
|
+
month: (date.getMonth() + 1).toString().padStart(2, "0"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async saveFile(params) {
|
|
51
|
+
const { dir, file, createdBy, date = new Date() } = params;
|
|
52
|
+
this.assertDir(dir);
|
|
53
|
+
const { year, month } = this.buildDatePathParts(date);
|
|
54
|
+
const fileDir = join(this.getBaseFileDir(), dir, year, month);
|
|
55
|
+
const storedFile = await StoreManager.saveFile(file, fileDir);
|
|
56
|
+
const relativePath = storedFile.path;
|
|
57
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
58
|
+
const extension = StoreManager.getExtension(storedFile.filename);
|
|
59
|
+
const url = `${this.getBaseUrl()}/api/file/${dir}/${year}/${month}/${storedFile.filename}`;
|
|
60
|
+
const type = storedFile.mimetype?.split("/")[0] || "";
|
|
61
|
+
if (this.isMetadataEnabled()) {
|
|
62
|
+
try {
|
|
63
|
+
await FileServiceFactory.instance.registerUploadedFile({
|
|
64
|
+
filename: storedFile.filename,
|
|
65
|
+
relativePath,
|
|
66
|
+
absolutePath,
|
|
67
|
+
size: storedFile.size,
|
|
68
|
+
mimetype: storedFile.mimetype || file.mimetype,
|
|
69
|
+
encoding: storedFile.encoding || file.encoding || "",
|
|
70
|
+
extension,
|
|
71
|
+
type,
|
|
72
|
+
lastAccess: new Date(),
|
|
73
|
+
ttlSeconds: 0,
|
|
74
|
+
hits: 0,
|
|
75
|
+
url,
|
|
76
|
+
createdBy,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
await StoreManager.deleteFile(fileDir, storedFile.filename).catch(() => undefined);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...storedFile,
|
|
86
|
+
fileDir,
|
|
87
|
+
relativePath,
|
|
88
|
+
absolutePath,
|
|
89
|
+
extension,
|
|
90
|
+
type,
|
|
91
|
+
url,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async getFile(params) {
|
|
95
|
+
const { dir, year, month, filename, registerHit = true } = params;
|
|
96
|
+
this.assertDir(dir);
|
|
97
|
+
this.assertYear(year);
|
|
98
|
+
this.assertMonth(month);
|
|
99
|
+
const fileDir = join(this.getBaseFileDir(), dir, year, month);
|
|
100
|
+
const relativePath = join(fileDir, filename);
|
|
101
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
102
|
+
try {
|
|
103
|
+
await access(absolutePath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new NotFoundError("File not found");
|
|
107
|
+
}
|
|
108
|
+
if (registerHit && this.isMetadataEnabled()) {
|
|
109
|
+
await FileServiceFactory.instance.registerDownloadHit(relativePath);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
dir,
|
|
113
|
+
year,
|
|
114
|
+
month,
|
|
115
|
+
filename,
|
|
116
|
+
fileDir,
|
|
117
|
+
relativePath,
|
|
118
|
+
absolutePath,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export default MediaService;
|
|
123
|
+
export { MediaService, };
|
package/env.d.ts
ADDED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.12.0",
|
|
7
7
|
"description": "Media files",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@drax/common-back": "^3.10.0",
|
|
24
|
-
"@drax/identity-back": "^3.
|
|
24
|
+
"@drax/identity-back": "^3.12.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@fastify/multipart": "^9.0.3",
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"tsc-alias": "^1.8.10",
|
|
40
40
|
"typescript": "^5.4.5"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "5395a022d01165f5c4cd11adbae6ed8b5d8b670a"
|
|
43
43
|
}
|
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CommonController,
|
|
3
|
-
StoreManager,
|
|
4
|
-
DraxConfig,
|
|
5
|
-
CommonConfig,
|
|
6
3
|
} from "@drax/common-back";
|
|
7
|
-
import {join} from "path";
|
|
8
4
|
import {MediaPermissions} from "../permissions/MediaPermissions.js";
|
|
9
|
-
import {
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
12
|
-
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : ''
|
|
5
|
+
import {MediaService} from "../services/MediaService.js";
|
|
13
6
|
|
|
14
7
|
class MediaController extends CommonController {
|
|
8
|
+
protected mediaService: MediaService;
|
|
15
9
|
|
|
16
10
|
constructor() {
|
|
17
11
|
super()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
validateDir(dir: string) {
|
|
21
|
-
let dirRegExp = /^[a-zA-Z0-9_-]+$/
|
|
22
|
-
if (!dir || dirRegExp.test(dir) === false) {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
return true
|
|
12
|
+
this.mediaService = new MediaService()
|
|
26
13
|
}
|
|
27
14
|
|
|
28
15
|
async uploadFile(request: any, reply: any) {
|
|
@@ -35,61 +22,24 @@ class MediaController extends CommonController {
|
|
|
35
22
|
}
|
|
36
23
|
|
|
37
24
|
const dir = request.params.dir
|
|
38
|
-
if (!this.validateDir(dir)) {
|
|
39
|
-
reply.statusCode = 400
|
|
40
|
-
reply.send({error: 'Invalid directory name'})
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
25
|
const data = await request.file()
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const storedFile = await StoreManager.saveFile(file, destinationPath)
|
|
56
|
-
const urlFile = `${BASE_URL}/api/file/${dir}/${year}/${month}/${storedFile.filename}`
|
|
57
|
-
const relativePath = storedFile.path
|
|
58
|
-
const absolutePath = path.resolve(process.cwd(), relativePath);
|
|
59
|
-
const extension = StoreManager.getExtension(storedFile.filename)
|
|
60
|
-
const fileService = FileServiceFactory.instance
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const FILE_METADATA = process.env.DRAX_FILE_METADATA ? (/true|yes|enable/i).test(process.env.DRAX_FILE_METADATA) : true
|
|
64
|
-
if (FILE_METADATA === true) {
|
|
65
|
-
try {
|
|
66
|
-
await fileService.registerUploadedFile({
|
|
67
|
-
filename: storedFile.filename,
|
|
68
|
-
relativePath: relativePath,
|
|
69
|
-
absolutePath: absolutePath,
|
|
70
|
-
size: storedFile.size,
|
|
71
|
-
mimetype: storedFile.mimetype || data.mimetype,
|
|
72
|
-
encoding: storedFile.encoding || data.encoding || '',
|
|
73
|
-
extension,
|
|
74
|
-
type: storedFile.mimetype?.split('/')[0] || '',
|
|
75
|
-
lastAccess: new Date(),
|
|
76
|
-
ttlSeconds: 0,
|
|
77
|
-
hits: 0,
|
|
78
|
-
url: urlFile,
|
|
79
|
-
createdBy,
|
|
80
|
-
})
|
|
81
|
-
} catch (e) {
|
|
82
|
-
await StoreManager.deleteFile(destinationPath, storedFile.filename).catch(() => undefined)
|
|
83
|
-
throw e
|
|
84
|
-
}
|
|
85
|
-
}
|
|
26
|
+
const storedFile = await this.mediaService.saveFile({
|
|
27
|
+
dir,
|
|
28
|
+
file: {
|
|
29
|
+
filename: data.filename,
|
|
30
|
+
fileStream: data.file,
|
|
31
|
+
mimetype: data.mimetype,
|
|
32
|
+
encoding: data.encoding,
|
|
33
|
+
},
|
|
34
|
+
createdBy,
|
|
35
|
+
})
|
|
86
36
|
|
|
87
37
|
let theFile = {
|
|
88
38
|
filename: storedFile.filename,
|
|
89
|
-
filepath: storedFile.
|
|
39
|
+
filepath: storedFile.relativePath,
|
|
90
40
|
size: storedFile.size,
|
|
91
41
|
mimetype: storedFile.mimetype,
|
|
92
|
-
url:
|
|
42
|
+
url: storedFile.url,
|
|
93
43
|
}
|
|
94
44
|
|
|
95
45
|
return theFile
|
|
@@ -106,38 +56,8 @@ class MediaController extends CommonController {
|
|
|
106
56
|
const year = request.params.year
|
|
107
57
|
const month = request.params.month
|
|
108
58
|
const filename = request.params.filename
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (this.validateDir(dir) == false) {
|
|
113
|
-
reply.statusCode = 400
|
|
114
|
-
reply.send({error: 'Invalid directory name'})
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (/[0-9]{4}/.test(year) == false) {
|
|
119
|
-
reply.statusCode = 400
|
|
120
|
-
reply.send({error: 'Invalid year'})
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (/[0-9]{2}/.test(month) == false) {
|
|
125
|
-
reply.statusCode = 400
|
|
126
|
-
reply.send({error: 'Invalid month'})
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const fileDir = join(BASE_FILE_DIR, dir, year, month)
|
|
131
|
-
//console.log("FILE_DIR: ", fileDir, " FILENAME:", filename)
|
|
132
|
-
|
|
133
|
-
//Agregar hit al archivo
|
|
134
|
-
const FILE_METADATA = process.env.DRAX_FILE_METADATA ? (/true|yes|enable/i).test(process.env.DRAX_FILE_METADATA) : true
|
|
135
|
-
if (FILE_METADATA === true) {
|
|
136
|
-
const fileService = FileServiceFactory.instance
|
|
137
|
-
await fileService.registerDownloadHit(join(fileDir, filename))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return reply.sendFile(filename, fileDir)
|
|
59
|
+
const file = await this.mediaService.getFile({dir, year, month, filename})
|
|
60
|
+
return reply.sendFile(file.filename, file.fileDir)
|
|
141
61
|
} catch (e) {
|
|
142
62
|
this.handleError(e, reply)
|
|
143
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,15 +7,28 @@ import FileModel from "./models/FileModel.js";
|
|
|
7
7
|
import FileMongoRepository from "./repository/mongo/FileMongoRepository.js";
|
|
8
8
|
import FileSqliteRepository from "./repository/sqlite/FileSqliteRepository.js";
|
|
9
9
|
import FileService from "./services/FileService.js";
|
|
10
|
+
import MediaService from "./services/MediaService.js";
|
|
10
11
|
import FileServiceFactory from "./factory/services/FileServiceFactory.js";
|
|
11
12
|
import FileController from "./controllers/FileController.js";
|
|
12
13
|
import type { IFile, IFileBase } from "./interfaces/IFile";
|
|
13
14
|
import type { IFileRepository } from "./interfaces/IFileRepository";
|
|
15
|
+
import type {
|
|
16
|
+
IMediaCreatedBy,
|
|
17
|
+
IMediaSaveFileParams,
|
|
18
|
+
IMediaSaveFileResult,
|
|
19
|
+
IMediaGetFileParams,
|
|
20
|
+
IMediaGetFileResult,
|
|
21
|
+
} from "./services/MediaService.js";
|
|
14
22
|
|
|
15
23
|
export type {
|
|
16
24
|
IFile,
|
|
17
25
|
IFileBase,
|
|
18
|
-
IFileRepository
|
|
26
|
+
IFileRepository,
|
|
27
|
+
IMediaCreatedBy,
|
|
28
|
+
IMediaSaveFileParams,
|
|
29
|
+
IMediaSaveFileResult,
|
|
30
|
+
IMediaGetFileParams,
|
|
31
|
+
IMediaGetFileResult,
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export {
|
|
@@ -33,6 +46,7 @@ export {
|
|
|
33
46
|
FileMongoRepository,
|
|
34
47
|
FileSqliteRepository,
|
|
35
48
|
FileService,
|
|
49
|
+
MediaService,
|
|
36
50
|
FileServiceFactory,
|
|
37
51
|
FileController
|
|
38
52
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestError,
|
|
3
|
+
CommonConfig,
|
|
4
|
+
DraxConfig,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
StoreManager,
|
|
7
|
+
} from "@drax/common-back";
|
|
8
|
+
import type {IUploadFile, IUploadFileResult} from "@drax/common-back";
|
|
9
|
+
import {join, resolve} from "node:path";
|
|
10
|
+
import {access} from "node:fs/promises";
|
|
11
|
+
import {FileServiceFactory} from "../factory/services/FileServiceFactory.js";
|
|
12
|
+
|
|
13
|
+
interface IMediaCreatedBy {
|
|
14
|
+
id: string;
|
|
15
|
+
username: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface IMediaSaveFileParams {
|
|
19
|
+
dir: string;
|
|
20
|
+
file: IUploadFile;
|
|
21
|
+
createdBy?: IMediaCreatedBy;
|
|
22
|
+
date?: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IMediaSaveFileResult extends IUploadFileResult {
|
|
26
|
+
fileDir: string;
|
|
27
|
+
relativePath: string;
|
|
28
|
+
absolutePath: string;
|
|
29
|
+
extension: string;
|
|
30
|
+
type: string;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface IMediaGetFileParams {
|
|
35
|
+
dir: string;
|
|
36
|
+
year: string;
|
|
37
|
+
month: string;
|
|
38
|
+
filename: string;
|
|
39
|
+
registerHit?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IMediaGetFileResult {
|
|
43
|
+
dir: string;
|
|
44
|
+
year: string;
|
|
45
|
+
month: string;
|
|
46
|
+
filename: string;
|
|
47
|
+
fileDir: string;
|
|
48
|
+
relativePath: string;
|
|
49
|
+
absolutePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class MediaService {
|
|
53
|
+
protected getBaseFileDir(): string {
|
|
54
|
+
return DraxConfig.getOrLoad(CommonConfig.FileDir) || "files";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected getBaseUrl(): string {
|
|
58
|
+
return DraxConfig.getOrLoad(CommonConfig.BaseUrl)
|
|
59
|
+
? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, "")
|
|
60
|
+
: "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validateDir(dir: string): boolean {
|
|
64
|
+
const dirRegExp = /^[a-zA-Z0-9_-]+$/;
|
|
65
|
+
return !!dir && dirRegExp.test(dir);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
validateYear(year: string): boolean {
|
|
69
|
+
return /^[0-9]{4}$/.test(year);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
validateMonth(month: string): boolean {
|
|
73
|
+
return /^[0-9]{2}$/.test(month);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected assertDir(dir: string): void {
|
|
77
|
+
if (!this.validateDir(dir)) {
|
|
78
|
+
throw new BadRequestError("Invalid directory name");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected assertYear(year: string): void {
|
|
83
|
+
if (!this.validateYear(year)) {
|
|
84
|
+
throw new BadRequestError("Invalid year");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected assertMonth(month: string): void {
|
|
89
|
+
if (!this.validateMonth(month)) {
|
|
90
|
+
throw new BadRequestError("Invalid month");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected isMetadataEnabled(): boolean {
|
|
95
|
+
return process.env.DRAX_FILE_METADATA
|
|
96
|
+
? /true|yes|enable/i.test(process.env.DRAX_FILE_METADATA)
|
|
97
|
+
: true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected buildDatePathParts(date: Date): {year: string; month: string} {
|
|
101
|
+
return {
|
|
102
|
+
year: date.getFullYear().toString(),
|
|
103
|
+
month: (date.getMonth() + 1).toString().padStart(2, "0"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async saveFile(params: IMediaSaveFileParams): Promise<IMediaSaveFileResult> {
|
|
108
|
+
const {dir, file, createdBy, date = new Date()} = params;
|
|
109
|
+
|
|
110
|
+
this.assertDir(dir);
|
|
111
|
+
|
|
112
|
+
const {year, month} = this.buildDatePathParts(date);
|
|
113
|
+
const fileDir = join(this.getBaseFileDir(), dir, year, month);
|
|
114
|
+
const storedFile = await StoreManager.saveFile(file, fileDir);
|
|
115
|
+
const relativePath = storedFile.path;
|
|
116
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
117
|
+
const extension = StoreManager.getExtension(storedFile.filename);
|
|
118
|
+
const url = `${this.getBaseUrl()}/api/file/${dir}/${year}/${month}/${storedFile.filename}`;
|
|
119
|
+
const type = storedFile.mimetype?.split("/")[0] || "";
|
|
120
|
+
|
|
121
|
+
if (this.isMetadataEnabled()) {
|
|
122
|
+
try {
|
|
123
|
+
await FileServiceFactory.instance.registerUploadedFile({
|
|
124
|
+
filename: storedFile.filename,
|
|
125
|
+
relativePath,
|
|
126
|
+
absolutePath,
|
|
127
|
+
size: storedFile.size,
|
|
128
|
+
mimetype: storedFile.mimetype || file.mimetype,
|
|
129
|
+
encoding: storedFile.encoding || file.encoding || "",
|
|
130
|
+
extension,
|
|
131
|
+
type,
|
|
132
|
+
lastAccess: new Date(),
|
|
133
|
+
ttlSeconds: 0,
|
|
134
|
+
hits: 0,
|
|
135
|
+
url,
|
|
136
|
+
createdBy,
|
|
137
|
+
});
|
|
138
|
+
} catch (e) {
|
|
139
|
+
await StoreManager.deleteFile(fileDir, storedFile.filename).catch(() => undefined);
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...storedFile,
|
|
146
|
+
fileDir,
|
|
147
|
+
relativePath,
|
|
148
|
+
absolutePath,
|
|
149
|
+
extension,
|
|
150
|
+
type,
|
|
151
|
+
url,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getFile(params: IMediaGetFileParams): Promise<IMediaGetFileResult> {
|
|
156
|
+
const {dir, year, month, filename, registerHit = true} = params;
|
|
157
|
+
|
|
158
|
+
this.assertDir(dir);
|
|
159
|
+
this.assertYear(year);
|
|
160
|
+
this.assertMonth(month);
|
|
161
|
+
|
|
162
|
+
const fileDir = join(this.getBaseFileDir(), dir, year, month);
|
|
163
|
+
const relativePath = join(fileDir, filename);
|
|
164
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await access(absolutePath);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new NotFoundError("File not found");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (registerHit && this.isMetadataEnabled()) {
|
|
173
|
+
await FileServiceFactory.instance.registerDownloadHit(relativePath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
dir,
|
|
178
|
+
year,
|
|
179
|
+
month,
|
|
180
|
+
filename,
|
|
181
|
+
fileDir,
|
|
182
|
+
relativePath,
|
|
183
|
+
absolutePath,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default MediaService;
|
|
189
|
+
export {
|
|
190
|
+
MediaService,
|
|
191
|
+
};
|
|
192
|
+
export type {
|
|
193
|
+
IMediaCreatedBy,
|
|
194
|
+
IMediaSaveFileParams,
|
|
195
|
+
IMediaSaveFileResult,
|
|
196
|
+
IMediaGetFileParams,
|
|
197
|
+
IMediaGetFileResult,
|
|
198
|
+
};
|