@drax/media-back 2.11.0 → 3.1.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/FileController.js +38 -0
- package/dist/controllers/MediaController.js +42 -1
- package/dist/factory/services/FileServiceFactory.js +30 -0
- package/dist/index.js +13 -2
- package/dist/interfaces/IFile.js +1 -0
- package/dist/interfaces/IFileRepository.js +1 -0
- package/dist/models/FileModel.js +49 -0
- package/dist/permissions/FilePermissions.js +13 -0
- package/dist/repository/mongo/FileMongoRepository.js +13 -0
- package/dist/repository/sqlite/FileSqliteRepository.js +37 -0
- package/dist/routes/FileRoutes.js +20 -0
- package/dist/schemas/FileSchema.js +51 -0
- package/dist/services/FileService.js +22 -0
- package/package.json +4 -4
- package/src/controllers/FileController.ts +50 -0
- package/src/controllers/MediaController.ts +48 -4
- package/src/factory/services/FileServiceFactory.ts +41 -0
- package/src/index.ts +30 -3
- package/src/interfaces/IFile.ts +67 -0
- package/src/interfaces/IFileRepository.ts +11 -0
- package/src/models/FileModel.ts +65 -0
- package/src/permissions/FilePermissions.ts +17 -0
- package/src/repository/mongo/FileMongoRepository.ts +22 -0
- package/src/repository/sqlite/FileSqliteRepository.ts +46 -0
- package/src/routes/FileRoutes.ts +36 -0
- package/src/schemas/FileSchema.ts +55 -0
- package/src/services/FileService.ts +36 -0
- package/test/modules/media/File/File-endpoints.test.ts +573 -0
- package/test/setup/MongoInMemory.ts +56 -0
- package/test/setup/TestSetup.ts +347 -0
- package/test/setup/data/admin-role.ts +13 -0
- package/test/setup/data/basic-user.ts +14 -0
- package/test/setup/data/file-role.ts +16 -0
- package/test/setup/data/one-tenant.ts +6 -0
- package/test/setup/data/restricted-role.ts +11 -0
- package/test/setup/data/root-user.ts +14 -0
- package/test/setup/data/tenant-one-user.ts +13 -0
- package/test/setup/data/tenant-two-user.ts +14 -0
- package/test/setup/data/two-tenant.ts +6 -0
- package/test/utils/StoreManager.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/types/controllers/FileController.d.ts +10 -0
- package/types/controllers/FileController.d.ts.map +1 -0
- package/types/controllers/MediaController.d.ts.map +1 -1
- package/types/factory/services/FileServiceFactory.d.ts +8 -0
- package/types/factory/services/FileServiceFactory.d.ts.map +1 -0
- package/types/index.d.ts +13 -1
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/IFile.d.ts +63 -0
- package/types/interfaces/IFile.d.ts.map +1 -0
- package/types/interfaces/IFileRepository.d.ts +6 -0
- package/types/interfaces/IFileRepository.d.ts.map +1 -0
- package/types/models/FileModel.d.ts +15 -0
- package/types/models/FileModel.d.ts.map +1 -0
- package/types/permissions/FilePermissions.d.ts +13 -0
- package/types/permissions/FilePermissions.d.ts.map +1 -0
- package/types/repository/mongo/FileMongoRepository.d.ts +9 -0
- package/types/repository/mongo/FileMongoRepository.d.ts.map +1 -0
- package/types/repository/sqlite/FileSqliteRepository.d.ts +18 -0
- package/types/repository/sqlite/FileSqliteRepository.d.ts.map +1 -0
- package/types/routes/FileRoutes.d.ts +4 -0
- package/types/routes/FileRoutes.d.ts.map +1 -0
- package/types/schemas/FileSchema.d.ts +65 -0
- package/types/schemas/FileSchema.d.ts.map +1 -0
- package/types/services/FileService.d.ts +12 -0
- package/types/services/FileService.d.ts.map +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import FileServiceFactory from "../factory/services/FileServiceFactory.js";
|
|
2
|
+
import { AbstractFastifyController } from "@drax/crud-back";
|
|
3
|
+
import FilePermissions from "../permissions/FilePermissions.js";
|
|
4
|
+
import { StoreManager } from "@drax/common-back";
|
|
5
|
+
class FileController extends AbstractFastifyController {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(FileServiceFactory.instance, FilePermissions);
|
|
8
|
+
this.tenantField = "tenant";
|
|
9
|
+
this.userField = "createdBy.id";
|
|
10
|
+
this.tenantFilter = true;
|
|
11
|
+
this.tenantSetter = true;
|
|
12
|
+
this.tenantAssert = true;
|
|
13
|
+
this.userFilter = true;
|
|
14
|
+
this.userSetter = true;
|
|
15
|
+
this.userAssert = true;
|
|
16
|
+
}
|
|
17
|
+
async preUpdate(request, payload) {
|
|
18
|
+
delete payload.relativePath;
|
|
19
|
+
delete payload.absolutePath;
|
|
20
|
+
delete payload.filename;
|
|
21
|
+
payload.updatedBy = {
|
|
22
|
+
id: request.rbac.userId,
|
|
23
|
+
username: request.rbac.username,
|
|
24
|
+
};
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
async postDelete(request, item) {
|
|
28
|
+
try {
|
|
29
|
+
await StoreManager.deleteFilepath(item.relativePath);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(error);
|
|
33
|
+
}
|
|
34
|
+
return item;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export default FileController;
|
|
38
|
+
export { FileController };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CommonController, StoreManager, DraxConfig, CommonConfig, } from "@drax/common-back";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { MediaPermissions } from "../permissions/MediaPermissions.js";
|
|
4
|
+
import { FileServiceFactory } from "../factory/services/FileServiceFactory.js";
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
5
7
|
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : '';
|
|
6
8
|
class MediaController extends CommonController {
|
|
@@ -17,6 +19,10 @@ class MediaController extends CommonController {
|
|
|
17
19
|
async uploadFile(request, reply) {
|
|
18
20
|
try {
|
|
19
21
|
request.rbac.assertPermission(MediaPermissions.UploadFile);
|
|
22
|
+
const createdBy = {
|
|
23
|
+
id: request.rbac.userId,
|
|
24
|
+
username: request.rbac.username,
|
|
25
|
+
};
|
|
20
26
|
const dir = request.params.dir;
|
|
21
27
|
if (!this.validateDir(dir)) {
|
|
22
28
|
reply.statusCode = 400;
|
|
@@ -34,13 +40,42 @@ class MediaController extends CommonController {
|
|
|
34
40
|
const destinationPath = join(BASE_FILE_DIR, dir, year, month);
|
|
35
41
|
const storedFile = await StoreManager.saveFile(file, destinationPath);
|
|
36
42
|
const urlFile = `${BASE_URL}/api/file/${dir}/${year}/${month}/${storedFile.filename}`;
|
|
37
|
-
|
|
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
|
+
}
|
|
71
|
+
let theFile = {
|
|
38
72
|
filename: storedFile.filename,
|
|
39
73
|
filepath: storedFile.path,
|
|
40
74
|
size: storedFile.size,
|
|
41
75
|
mimetype: storedFile.mimetype,
|
|
42
76
|
url: urlFile,
|
|
43
77
|
};
|
|
78
|
+
return theFile;
|
|
44
79
|
}
|
|
45
80
|
catch (e) {
|
|
46
81
|
this.handleError(e, reply);
|
|
@@ -70,6 +105,12 @@ class MediaController extends CommonController {
|
|
|
70
105
|
}
|
|
71
106
|
const fileDir = join(BASE_FILE_DIR, dir, year, month);
|
|
72
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
|
+
}
|
|
73
114
|
return reply.sendFile(filename, fileDir);
|
|
74
115
|
}
|
|
75
116
|
catch (e) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import FileMongoRepository from '../../repository/mongo/FileMongoRepository.js';
|
|
2
|
+
import FileSqliteRepository from '../../repository/sqlite/FileSqliteRepository.js';
|
|
3
|
+
import { FileService } from '../../services/FileService.js';
|
|
4
|
+
import { FileBaseSchema, FileSchema } from "../../schemas/FileSchema.js";
|
|
5
|
+
import { COMMON, CommonConfig, DraxConfig } from "@drax/common-back";
|
|
6
|
+
class FileServiceFactory {
|
|
7
|
+
static get instance() {
|
|
8
|
+
if (!FileServiceFactory.service) {
|
|
9
|
+
let repository;
|
|
10
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
11
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
12
|
+
repository = new FileMongoRepository();
|
|
13
|
+
break;
|
|
14
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
15
|
+
const dbFile = DraxConfig.getOrLoad(CommonConfig.SqliteDbFile);
|
|
16
|
+
repository = new FileSqliteRepository(dbFile, false);
|
|
17
|
+
repository.build();
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
21
|
+
}
|
|
22
|
+
const baseSchema = FileBaseSchema;
|
|
23
|
+
const fullSchema = FileSchema;
|
|
24
|
+
FileServiceFactory.service = new FileService(repository, baseSchema, fullSchema);
|
|
25
|
+
}
|
|
26
|
+
return FileServiceFactory.service;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export default FileServiceFactory;
|
|
30
|
+
export { FileServiceFactory };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { MediaRoutes } from "./routes/MediaRoutes.js";
|
|
2
2
|
import { MediaPermissions } from "./permissions/MediaPermissions.js";
|
|
3
|
+
import { FileRoutes } from "./routes/FileRoutes.js";
|
|
4
|
+
import { FilePermissions } from "./permissions/FilePermissions.js";
|
|
5
|
+
import FileSchema from "./schemas/FileSchema.js";
|
|
6
|
+
import FileModel from "./models/FileModel.js";
|
|
7
|
+
import FileMongoRepository from "./repository/mongo/FileMongoRepository.js";
|
|
8
|
+
import FileSqliteRepository from "./repository/sqlite/FileSqliteRepository.js";
|
|
9
|
+
import FileService from "./services/FileService.js";
|
|
10
|
+
import FileServiceFactory from "./factory/services/FileServiceFactory.js";
|
|
11
|
+
import FileController from "./controllers/FileController.js";
|
|
3
12
|
export {
|
|
4
13
|
//Routes
|
|
5
|
-
MediaRoutes,
|
|
14
|
+
MediaRoutes, FileRoutes,
|
|
6
15
|
//Permissions
|
|
7
|
-
MediaPermissions,
|
|
16
|
+
MediaPermissions, FilePermissions,
|
|
17
|
+
//File
|
|
18
|
+
FileSchema, FileModel, FileMongoRepository, FileSqliteRepository, FileService, FileServiceFactory, FileController };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mongoose } from '@drax/common-back';
|
|
2
|
+
import uniqueValidator from 'mongoose-unique-validator';
|
|
3
|
+
import mongoosePaginate from 'mongoose-paginate-v2';
|
|
4
|
+
const FileSchema = new mongoose.Schema({
|
|
5
|
+
filename: { type: String, required: true, index: false, unique: false },
|
|
6
|
+
relativePath: { type: String, required: true, index: true, unique: false },
|
|
7
|
+
absolutePath: { type: String, required: true, index: false, unique: false },
|
|
8
|
+
url: { type: String, required: true, index: false, unique: false },
|
|
9
|
+
description: { type: String, required: false, index: false, unique: false },
|
|
10
|
+
tags: [{ type: String, required: false, index: false, unique: false }],
|
|
11
|
+
mimetype: { type: String, required: true, index: false, unique: false },
|
|
12
|
+
encoding: { type: String, required: false, index: false, unique: false },
|
|
13
|
+
extension: { type: String, required: false, index: false, unique: false },
|
|
14
|
+
size: { type: Number, required: true, index: false, unique: false },
|
|
15
|
+
type: { type: String, required: false, index: false, unique: false },
|
|
16
|
+
lastAccess: { type: Date, required: false, index: false, unique: false },
|
|
17
|
+
createdBy: {
|
|
18
|
+
id: { type: mongoose.Schema.Types.ObjectId, required: false, index: false, unique: false },
|
|
19
|
+
username: { type: String, required: false, index: false, unique: false }
|
|
20
|
+
},
|
|
21
|
+
updatedBy: {
|
|
22
|
+
id: { type: mongoose.Schema.Types.ObjectId, required: false, index: false, unique: false },
|
|
23
|
+
username: { type: String, required: false, index: false, unique: false }
|
|
24
|
+
},
|
|
25
|
+
createdFor: { type: String, required: false, index: false, unique: false },
|
|
26
|
+
ttlSeconds: { type: Number, required: false, index: false, unique: false },
|
|
27
|
+
expiresAt: { type: Date, required: false, index: false, unique: false },
|
|
28
|
+
isPublic: { type: Boolean, required: false, index: false, unique: false },
|
|
29
|
+
hits: { type: Number, required: false, index: false, unique: false },
|
|
30
|
+
tenant: { type: mongoose.Schema.Types.ObjectId, ref: 'Tenant', required: false, index: false, unique: false },
|
|
31
|
+
}, { timestamps: true });
|
|
32
|
+
FileSchema.plugin(uniqueValidator, { message: 'validation.unique' });
|
|
33
|
+
FileSchema.plugin(mongoosePaginate);
|
|
34
|
+
FileSchema.virtual("id").get(function () {
|
|
35
|
+
return this._id.toString();
|
|
36
|
+
});
|
|
37
|
+
FileSchema.set('toJSON', { getters: true, virtuals: true });
|
|
38
|
+
FileSchema.set('toObject', { getters: true, virtuals: true });
|
|
39
|
+
FileSchema.pre("validate", function (next) {
|
|
40
|
+
if (this.ttlSeconds && !this.expiresAt) {
|
|
41
|
+
this.expiresAt = new Date(Date.now() + this.ttlSeconds * 1000);
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
45
|
+
const MODEL_NAME = 'File';
|
|
46
|
+
const COLLECTION_NAME = 'File';
|
|
47
|
+
const FileModel = mongoose.model(MODEL_NAME, FileSchema, COLLECTION_NAME);
|
|
48
|
+
export { FileSchema, FileModel };
|
|
49
|
+
export default FileModel;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var FilePermissions;
|
|
2
|
+
(function (FilePermissions) {
|
|
3
|
+
FilePermissions["Create"] = "file:create";
|
|
4
|
+
FilePermissions["Update"] = "file:update";
|
|
5
|
+
FilePermissions["Delete"] = "file:delete";
|
|
6
|
+
FilePermissions["View"] = "file:view";
|
|
7
|
+
FilePermissions["ViewAll"] = "file:viewAll";
|
|
8
|
+
FilePermissions["UpdateAll"] = "file:updateAll";
|
|
9
|
+
FilePermissions["DeleteAll"] = "file:deleteAll";
|
|
10
|
+
FilePermissions["Manage"] = "file:manage";
|
|
11
|
+
})(FilePermissions || (FilePermissions = {}));
|
|
12
|
+
export { FilePermissions };
|
|
13
|
+
export default FilePermissions;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AbstractMongoRepository } from "@drax/crud-back";
|
|
2
|
+
import { FileModel } from "../../models/FileModel.js";
|
|
3
|
+
class FileMongoRepository extends AbstractMongoRepository {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this._model = FileModel;
|
|
7
|
+
this._searchFields = ['filename', 'url', 'description', 'tags', 'mimetype', 'extension', 'type'];
|
|
8
|
+
this._populateFields = ['tenant'];
|
|
9
|
+
this._lean = true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export default FileMongoRepository;
|
|
13
|
+
export { FileMongoRepository };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AbstractSqliteRepository } from "@drax/crud-back";
|
|
2
|
+
class FileSqliteRepository extends AbstractSqliteRepository {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.tableName = 'File';
|
|
6
|
+
this.searchFields = ['filename', 'relativePath', 'absolutePath', 'url', 'description', 'tags', 'mimetype', 'extension', 'type'];
|
|
7
|
+
this.booleanFields = ['isPublic'];
|
|
8
|
+
this.identifier = '_id';
|
|
9
|
+
this.populateFields = [];
|
|
10
|
+
this.verbose = false;
|
|
11
|
+
this.tableFields = [
|
|
12
|
+
{ name: "filename", type: "TEXT", unique: undefined, primary: false },
|
|
13
|
+
{ name: "relativePath", type: "TEXT", unique: undefined, primary: false },
|
|
14
|
+
{ name: "absolutePath", type: "TEXT", unique: undefined, primary: false },
|
|
15
|
+
{ name: "url", type: "TEXT", unique: undefined, primary: false },
|
|
16
|
+
{ name: "description", type: "TEXT", unique: undefined, primary: false },
|
|
17
|
+
{ name: "tags", type: "TEXT", unique: undefined, primary: false },
|
|
18
|
+
{ name: "mimetype", type: "TEXT", unique: undefined, primary: false },
|
|
19
|
+
{ name: "encoding", type: "TEXT", unique: undefined, primary: false },
|
|
20
|
+
{ name: "extension", type: "TEXT", unique: undefined, primary: false },
|
|
21
|
+
{ name: "size", type: "REAL", unique: undefined, primary: false },
|
|
22
|
+
{ name: "type", type: "TEXT", unique: undefined, primary: false },
|
|
23
|
+
{ name: "lastAccess", type: "TEXT", unique: undefined, primary: false },
|
|
24
|
+
{ name: "createdAt", type: "TEXT", unique: undefined, primary: false },
|
|
25
|
+
{ name: "updatedAt", type: "TEXT", unique: undefined, primary: false },
|
|
26
|
+
{ name: "createdBy", type: "TEXT", unique: undefined, primary: false },
|
|
27
|
+
{ name: "updatedBy", type: "TEXT", unique: undefined, primary: false },
|
|
28
|
+
{ name: "createdFor", type: "TEXT", unique: undefined, primary: false },
|
|
29
|
+
{ name: "ttlSeconds", type: "REAL", unique: undefined, primary: false },
|
|
30
|
+
{ name: "expiresAt", type: "TEXT", unique: undefined, primary: false },
|
|
31
|
+
{ name: "isPublic", type: "TEXT", unique: undefined, primary: false },
|
|
32
|
+
{ name: "hits", type: "REAL", unique: undefined, primary: false }
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default FileSqliteRepository;
|
|
37
|
+
export { FileSqliteRepository };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import FileController from "../controllers/FileController.js";
|
|
2
|
+
import { CrudSchemaBuilder } from "@drax/crud-back";
|
|
3
|
+
import { FileSchema, FileBaseSchema } from '../schemas/FileSchema.js';
|
|
4
|
+
async function FileRoutes(fastify, options) {
|
|
5
|
+
const controller = new FileController();
|
|
6
|
+
const schemas = new CrudSchemaBuilder(FileSchema, FileBaseSchema, FileBaseSchema, 'File', 'openapi-3.0', ['media']);
|
|
7
|
+
fastify.get('/api/file', { schema: schemas.paginateSchema }, (req, rep) => controller.paginate(req, rep));
|
|
8
|
+
fastify.get('/api/file/find', { schema: schemas.findSchema }, (req, rep) => controller.find(req, rep));
|
|
9
|
+
fastify.get('/api/file/search', { schema: schemas.searchSchema }, (req, rep) => controller.search(req, rep));
|
|
10
|
+
fastify.get('/api/file/:id', { schema: schemas.findByIdSchema }, (req, rep) => controller.findById(req, rep));
|
|
11
|
+
fastify.get('/api/file/find-one', { schema: schemas.findOneSchema }, (req, rep) => controller.findOne(req, rep));
|
|
12
|
+
fastify.get('/api/file/group-by', { schema: schemas.groupBySchema }, (req, rep) => controller.groupBy(req, rep));
|
|
13
|
+
fastify.post('/api/file', { schema: schemas.createSchema }, (req, rep) => controller.create(req, rep));
|
|
14
|
+
fastify.put('/api/file/:id', { schema: schemas.updateSchema }, (req, rep) => controller.update(req, rep));
|
|
15
|
+
fastify.patch('/api/file/:id', { schema: schemas.updateSchema }, (req, rep) => controller.updatePartial(req, rep));
|
|
16
|
+
fastify.delete('/api/file/:id', { schema: schemas.deleteSchema }, (req, rep) => controller.delete(req, rep));
|
|
17
|
+
fastify.get('/api/file/export', (req, rep) => controller.export(req, rep));
|
|
18
|
+
}
|
|
19
|
+
export default FileRoutes;
|
|
20
|
+
export { FileRoutes };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const FileBaseSchema = z.object({
|
|
3
|
+
filename: z.string().min(1, 'validation.required'),
|
|
4
|
+
relativePath: z.string().min(1, 'validation.required'),
|
|
5
|
+
absolutePath: z.string().min(1, 'validation.required'),
|
|
6
|
+
url: z.string().min(1, 'validation.required'),
|
|
7
|
+
description: z.string().default('').nullish(),
|
|
8
|
+
tags: z.array(z.string()).optional().default([]),
|
|
9
|
+
mimetype: z.string().nullish(),
|
|
10
|
+
encoding: z.string().nullish(),
|
|
11
|
+
extension: z.string().nullish(),
|
|
12
|
+
size: z.number().min(0, 'validation.required').nullish(),
|
|
13
|
+
type: z.string().nullish(),
|
|
14
|
+
lastAccess: z.coerce.date({ error: "validation.date" }).nullish(),
|
|
15
|
+
createdFor: z.string().nullish(),
|
|
16
|
+
ttlSeconds: z.number().default(0).nullish(),
|
|
17
|
+
expiresAt: z.coerce.date().nullish(),
|
|
18
|
+
isPublic: z.boolean().nullish(),
|
|
19
|
+
hits: z.number().default(0),
|
|
20
|
+
createdBy: z.object({
|
|
21
|
+
id: z.string().nullish(),
|
|
22
|
+
username: z.string().nullish()
|
|
23
|
+
}).nullish(),
|
|
24
|
+
updatedBy: z.object({
|
|
25
|
+
id: z.string().nullish(),
|
|
26
|
+
username: z.string().nullish()
|
|
27
|
+
}).nullish(),
|
|
28
|
+
tenant: z.coerce.string().nullish(),
|
|
29
|
+
});
|
|
30
|
+
const FileSchema = FileBaseSchema
|
|
31
|
+
.omit({ absolutePath: true })
|
|
32
|
+
.extend({
|
|
33
|
+
_id: z.coerce.string(),
|
|
34
|
+
createdAt: z.coerce.date(),
|
|
35
|
+
updatedAt: z.coerce.date(),
|
|
36
|
+
expiresAt: z.coerce.date().nullish(),
|
|
37
|
+
createdBy: z.object({
|
|
38
|
+
id: z.coerce.string().nullish(),
|
|
39
|
+
username: z.string().nullish()
|
|
40
|
+
}).nullish(),
|
|
41
|
+
updatedBy: z.object({
|
|
42
|
+
id: z.coerce.string().nullish(),
|
|
43
|
+
username: z.string().nullish()
|
|
44
|
+
}).nullish(),
|
|
45
|
+
tenant: z.object({
|
|
46
|
+
_id: z.coerce.string(),
|
|
47
|
+
name: z.string()
|
|
48
|
+
}).nullish()
|
|
49
|
+
});
|
|
50
|
+
export default FileSchema;
|
|
51
|
+
export { FileSchema, FileBaseSchema };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AbstractService } from "@drax/crud-back";
|
|
2
|
+
class FileService extends AbstractService {
|
|
3
|
+
constructor(FileRepository, baseSchema, fullSchema) {
|
|
4
|
+
super(FileRepository, baseSchema, fullSchema);
|
|
5
|
+
this._validateOutput = true;
|
|
6
|
+
}
|
|
7
|
+
async registerUploadedFile(data) {
|
|
8
|
+
return await this.create(data);
|
|
9
|
+
}
|
|
10
|
+
async registerDownloadHit(relativePath) {
|
|
11
|
+
const file = await this.findOneBy('relativePath', relativePath);
|
|
12
|
+
if (!file) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return await this.updatePartial(file._id, {
|
|
16
|
+
hits: (file.hits || 0) + 1,
|
|
17
|
+
lastAccess: new Date()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export default FileService;
|
|
22
|
+
export { FileService };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "3.1.0",
|
|
7
7
|
"description": "Media files",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"author": "Cristian Incarnato & Drax Team",
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@drax/common-back": "^
|
|
24
|
-
"@drax/identity-back": "^
|
|
23
|
+
"@drax/common-back": "^3.0.0",
|
|
24
|
+
"@drax/identity-back": "^3.1.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": "262ea8f861c84ca1ad4d555545c5a94b95dbf25e"
|
|
43
43
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import FileServiceFactory from "../factory/services/FileServiceFactory.js";
|
|
2
|
+
import {AbstractFastifyController, CustomRequest} from "@drax/crud-back";
|
|
3
|
+
import FilePermissions from "../permissions/FilePermissions.js";
|
|
4
|
+
import type {IFile, IFileBase} from "../interfaces/IFile";
|
|
5
|
+
import {StoreManager} from "@drax/common-back";
|
|
6
|
+
|
|
7
|
+
class FileController extends AbstractFastifyController<IFile, IFileBase, IFileBase> {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super(FileServiceFactory.instance, FilePermissions)
|
|
11
|
+
this.tenantField = "tenant";
|
|
12
|
+
this.userField = "createdBy.id";
|
|
13
|
+
|
|
14
|
+
this.tenantFilter = true;
|
|
15
|
+
this.tenantSetter = true;
|
|
16
|
+
this.tenantAssert = true;
|
|
17
|
+
|
|
18
|
+
this.userFilter = true;
|
|
19
|
+
this.userSetter = true;
|
|
20
|
+
this.userAssert = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async preUpdate(request: CustomRequest, payload:any):Promise<IFileBase>{
|
|
24
|
+
delete payload.relativePath;
|
|
25
|
+
delete payload.absolutePath;
|
|
26
|
+
delete payload.filename;
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
payload.updatedBy = {
|
|
30
|
+
id: request.rbac.userId,
|
|
31
|
+
username: request.rbac.username,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return payload
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async postDelete(request: CustomRequest, item:IFile){
|
|
38
|
+
try {
|
|
39
|
+
await StoreManager.deleteFilepath(item.relativePath)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(error)
|
|
42
|
+
}
|
|
43
|
+
return item
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default FileController;
|
|
48
|
+
export {
|
|
49
|
+
FileController
|
|
50
|
+
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CommonController,
|
|
3
3
|
StoreManager,
|
|
4
|
-
UnauthorizedError,
|
|
5
|
-
UploadFileError,
|
|
6
4
|
DraxConfig,
|
|
7
5
|
CommonConfig,
|
|
8
6
|
} from "@drax/common-back";
|
|
9
7
|
import {join} from "path";
|
|
10
8
|
import {MediaPermissions} from "../permissions/MediaPermissions.js";
|
|
11
|
-
|
|
9
|
+
import {FileServiceFactory} from "../factory/services/FileServiceFactory.js";
|
|
10
|
+
import path from 'node:path';
|
|
12
11
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
13
12
|
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : ''
|
|
14
13
|
|
|
@@ -30,6 +29,11 @@ class MediaController extends CommonController {
|
|
|
30
29
|
try {
|
|
31
30
|
request.rbac.assertPermission(MediaPermissions.UploadFile)
|
|
32
31
|
|
|
32
|
+
const createdBy = {
|
|
33
|
+
id: request.rbac.userId,
|
|
34
|
+
username: request.rbac.username,
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
const dir = request.params.dir
|
|
34
38
|
if (!this.validateDir(dir)) {
|
|
35
39
|
reply.statusCode = 400
|
|
@@ -50,13 +54,45 @@ class MediaController extends CommonController {
|
|
|
50
54
|
|
|
51
55
|
const storedFile = await StoreManager.saveFile(file, destinationPath)
|
|
52
56
|
const urlFile = `${BASE_URL}/api/file/${dir}/${year}/${month}/${storedFile.filename}`
|
|
53
|
-
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
let theFile = {
|
|
54
88
|
filename: storedFile.filename,
|
|
55
89
|
filepath: storedFile.path,
|
|
56
90
|
size: storedFile.size,
|
|
57
91
|
mimetype: storedFile.mimetype,
|
|
58
92
|
url: urlFile,
|
|
59
93
|
}
|
|
94
|
+
|
|
95
|
+
return theFile
|
|
60
96
|
} catch (e) {
|
|
61
97
|
this.handleError(e, reply)
|
|
62
98
|
}
|
|
@@ -93,6 +129,14 @@ class MediaController extends CommonController {
|
|
|
93
129
|
|
|
94
130
|
const fileDir = join(BASE_FILE_DIR, dir, year, month)
|
|
95
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
|
+
|
|
96
140
|
return reply.sendFile(filename, fileDir)
|
|
97
141
|
} catch (e) {
|
|
98
142
|
this.handleError(e, reply)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import FileMongoRepository from '../../repository/mongo/FileMongoRepository.js'
|
|
3
|
+
import FileSqliteRepository from '../../repository/sqlite/FileSqliteRepository.js'
|
|
4
|
+
import type {IFileRepository} from "../../interfaces/IFileRepository";
|
|
5
|
+
import {FileService} from '../../services/FileService.js'
|
|
6
|
+
import {FileBaseSchema, FileSchema} from "../../schemas/FileSchema.js";
|
|
7
|
+
import {COMMON, CommonConfig, DraxConfig} from "@drax/common-back";
|
|
8
|
+
|
|
9
|
+
class FileServiceFactory {
|
|
10
|
+
private static service: FileService;
|
|
11
|
+
|
|
12
|
+
public static get instance(): FileService {
|
|
13
|
+
if (!FileServiceFactory.service) {
|
|
14
|
+
|
|
15
|
+
let repository: IFileRepository
|
|
16
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
17
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
18
|
+
repository = new FileMongoRepository()
|
|
19
|
+
break;
|
|
20
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
21
|
+
const dbFile = DraxConfig.getOrLoad(CommonConfig.SqliteDbFile)
|
|
22
|
+
repository = new FileSqliteRepository(dbFile, false)
|
|
23
|
+
repository.build()
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const baseSchema = FileBaseSchema;
|
|
30
|
+
const fullSchema = FileSchema;
|
|
31
|
+
FileServiceFactory.service = new FileService(repository, baseSchema, fullSchema);
|
|
32
|
+
}
|
|
33
|
+
return FileServiceFactory.service;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default FileServiceFactory
|
|
38
|
+
export {
|
|
39
|
+
FileServiceFactory
|
|
40
|
+
}
|
|
41
|
+
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
|
-
import {MediaRoutes} from "./routes/MediaRoutes.js";
|
|
2
|
-
import {MediaPermissions} from "./permissions/MediaPermissions.js";
|
|
1
|
+
import { MediaRoutes } from "./routes/MediaRoutes.js";
|
|
2
|
+
import { MediaPermissions } from "./permissions/MediaPermissions.js";
|
|
3
|
+
import { FileRoutes } from "./routes/FileRoutes.js";
|
|
4
|
+
import { FilePermissions } from "./permissions/FilePermissions.js";
|
|
5
|
+
import FileSchema from "./schemas/FileSchema.js";
|
|
6
|
+
import FileModel from "./models/FileModel.js";
|
|
7
|
+
import FileMongoRepository from "./repository/mongo/FileMongoRepository.js";
|
|
8
|
+
import FileSqliteRepository from "./repository/sqlite/FileSqliteRepository.js";
|
|
9
|
+
import FileService from "./services/FileService.js";
|
|
10
|
+
import FileServiceFactory from "./factory/services/FileServiceFactory.js";
|
|
11
|
+
import FileController from "./controllers/FileController.js";
|
|
12
|
+
import type { IFile, IFileBase } from "./interfaces/IFile";
|
|
13
|
+
import type { IFileRepository } from "./interfaces/IFileRepository";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
IFile,
|
|
17
|
+
IFileBase,
|
|
18
|
+
IFileRepository
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
export {
|
|
5
22
|
//Routes
|
|
6
23
|
MediaRoutes,
|
|
24
|
+
FileRoutes,
|
|
7
25
|
|
|
8
26
|
//Permissions
|
|
9
27
|
MediaPermissions,
|
|
10
|
-
|
|
28
|
+
FilePermissions,
|
|
11
29
|
|
|
30
|
+
//File
|
|
31
|
+
FileSchema,
|
|
32
|
+
FileModel,
|
|
33
|
+
FileMongoRepository,
|
|
34
|
+
FileSqliteRepository,
|
|
35
|
+
FileService,
|
|
36
|
+
FileServiceFactory,
|
|
37
|
+
FileController
|
|
38
|
+
}
|