@programisto/edrm-storage 0.3.1
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 +135 -0
- package/dist/bin/www.d.ts +2 -0
- package/dist/bin/www.js +13 -0
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +9 -0
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +6 -0
- package/dist/modules/edrm-exams/lib/openai.d.ts +37 -0
- package/dist/modules/edrm-exams/lib/openai.js +135 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +167 -0
- package/dist/modules/edrm-exams/models/candidate.model.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.model.js +75 -0
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.models.js +75 -0
- package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
- package/dist/modules/edrm-exams/models/company.model.js +34 -0
- package/dist/modules/edrm-exams/models/contact.model.d.ts +14 -0
- package/dist/modules/edrm-exams/models/contact.model.js +60 -0
- package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
- package/dist/modules/edrm-exams/models/test-job.model.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-job.model.js +29 -0
- package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
- package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
- package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
- package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
- package/dist/modules/edrm-exams/models/test.model.d.ts +47 -0
- package/dist/modules/edrm-exams/models/test.model.js +133 -0
- package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
- package/dist/modules/edrm-exams/models/user.model.js +73 -0
- package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/company.router.js +108 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +448 -0
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +8 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +1343 -0
- package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/result.router.js +370 -0
- package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/user.router.js +96 -0
- package/dist/modules/edrm-storage/config/edrm-storage.config.d.ts +29 -0
- package/dist/modules/edrm-storage/config/edrm-storage.config.js +31 -0
- package/dist/modules/edrm-storage/config/environment.example.d.ts +54 -0
- package/dist/modules/edrm-storage/config/environment.example.js +130 -0
- package/dist/modules/edrm-storage/examples/usage.example.d.ts +52 -0
- package/dist/modules/edrm-storage/examples/usage.example.js +156 -0
- package/dist/modules/edrm-storage/index.d.ts +5 -0
- package/dist/modules/edrm-storage/index.js +8 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.d.ts +53 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.js +132 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.d.ts +35 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.js +1 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.d.ts +6 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.js +151 -0
- package/dist/modules/edrm-storage/models/file.model.d.ts +78 -0
- package/dist/modules/edrm-storage/models/file.model.js +190 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.d.ts +18 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.js +95 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.d.ts +8 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.js +155 -0
- package/dist/modules/edrm-storage/scripts/quick-start.d.ts +7 -0
- package/dist/modules/edrm-storage/scripts/quick-start.js +114 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.d.ts +29 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.js +188 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.js +143 -0
- package/dist/modules/edrm-storage/tests/integration.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/integration.test.js +141 -0
- package/package.json +81 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { EnduranceRouter } from '@programisto/endurance-core';
|
|
2
|
+
import { EdrmStorageService } from '../services/edrm-storage.service.js';
|
|
3
|
+
class EdrmStorageRouter extends EnduranceRouter {
|
|
4
|
+
storageService;
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.storageService = new EdrmStorageService();
|
|
8
|
+
}
|
|
9
|
+
setupRoutes() {
|
|
10
|
+
const adminOptions = {
|
|
11
|
+
requireAuth: true,
|
|
12
|
+
permissions: ['admin', 'file:manage']
|
|
13
|
+
};
|
|
14
|
+
const userOptions = {
|
|
15
|
+
requireAuth: true,
|
|
16
|
+
permissions: ['file:read']
|
|
17
|
+
};
|
|
18
|
+
// Routes publiques pour l'initialisation d'upload
|
|
19
|
+
this.post('/files/init', { requireAuth: true, permissions: ['file:upload'] }, async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const { originalName, mimeType, size, tenantId, entityName, entityId, provider = 'S3', metadata, tags } = req.body;
|
|
22
|
+
if (!originalName || !mimeType || !size || !tenantId || !entityName || !entityId) {
|
|
23
|
+
return res.status(400).json({
|
|
24
|
+
success: false,
|
|
25
|
+
message: 'Paramètres manquants: originalName, mimeType, size, tenantId, entityName, entityId'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const result = await this.storageService.initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider, metadata, tags);
|
|
29
|
+
return res.json({
|
|
30
|
+
success: true,
|
|
31
|
+
data: result
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Erreur lors de l\'initialisation de l\'upload:', error);
|
|
36
|
+
return res.status(500).json({
|
|
37
|
+
success: false,
|
|
38
|
+
message: 'Erreur lors de l\'initialisation de l\'upload'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Finaliser un upload
|
|
43
|
+
this.post('/files/:fileId/complete', adminOptions, async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { fileId } = req.params;
|
|
46
|
+
const result = await this.storageService.completeUpload(fileId);
|
|
47
|
+
return res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
data: result
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('Erreur lors de la finalisation de l\'upload:', error);
|
|
54
|
+
return res.status(500).json({
|
|
55
|
+
success: false,
|
|
56
|
+
message: 'Erreur lors de la finalisation de l\'upload'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Obtenir une URL de téléchargement
|
|
61
|
+
this.get('/files/:fileId/download', userOptions, async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const { fileId } = req.params;
|
|
64
|
+
const { filename, expiresIn = 3600 } = req.query;
|
|
65
|
+
const result = await this.storageService.getDownloadUrl(fileId, filename, parseInt(expiresIn));
|
|
66
|
+
return res.json({
|
|
67
|
+
success: true,
|
|
68
|
+
data: result
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Erreur lors de la génération de l\'URL de téléchargement:', error);
|
|
73
|
+
return res.status(500).json({
|
|
74
|
+
success: false,
|
|
75
|
+
message: 'Erreur lors de la génération de l\'URL de téléchargement'
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Supprimer un fichier
|
|
80
|
+
this.delete('/files/:fileId', adminOptions, async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const { fileId } = req.params;
|
|
83
|
+
await this.storageService.deleteFile(fileId);
|
|
84
|
+
return res.json({
|
|
85
|
+
success: true,
|
|
86
|
+
message: 'Fichier supprimé avec succès'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error('Erreur lors de la suppression du fichier:', error);
|
|
91
|
+
return res.status(500).json({
|
|
92
|
+
success: false,
|
|
93
|
+
message: 'Erreur lors de la suppression du fichier'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Lister les fichiers
|
|
98
|
+
this.get('/files', userOptions, async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const { tenantId, entityName, entityId, status, page = 1, limit = 20 } = req.query;
|
|
101
|
+
const result = await this.storageService.listFiles(tenantId, entityName, entityId, status, parseInt(page), parseInt(limit));
|
|
102
|
+
return res.json({
|
|
103
|
+
success: true,
|
|
104
|
+
data: result
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error('Erreur lors de la récupération des fichiers:', error);
|
|
109
|
+
return res.status(500).json({
|
|
110
|
+
success: false,
|
|
111
|
+
message: 'Erreur lors de la récupération des fichiers'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// Obtenir les détails d'un fichier
|
|
116
|
+
this.get('/files/:fileId', userOptions, async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const { fileId } = req.params;
|
|
119
|
+
const result = await this.storageService.getFileById(fileId);
|
|
120
|
+
return res.json({
|
|
121
|
+
success: true,
|
|
122
|
+
data: result
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error('Erreur lors de la récupération du fichier:', error);
|
|
127
|
+
return res.status(500).json({
|
|
128
|
+
success: false,
|
|
129
|
+
message: 'Erreur lors de la récupération du fichier'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// Route de callback pour les événements S3 (optionnel)
|
|
134
|
+
this.post('/files/s3-callback', { requireAuth: false, permissions: [] }, async (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
// Cette route peut être utilisée pour recevoir des webhooks S3
|
|
137
|
+
// et déclencher des traitements asynchrones
|
|
138
|
+
console.log('S3 callback received:', req.body);
|
|
139
|
+
return res.json({
|
|
140
|
+
success: true,
|
|
141
|
+
message: 'Callback traité avec succès'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error('Erreur lors du traitement du callback S3:', error);
|
|
146
|
+
return res.status(500).json({
|
|
147
|
+
success: false,
|
|
148
|
+
message: 'Erreur lors du traitement du callback'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const router = new EdrmStorageRouter();
|
|
155
|
+
export default router;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Script de démarrage rapide pour tester le module EDRM Storage
|
|
4
|
+
* Usage: npm run edrm-storage:test
|
|
5
|
+
*/
|
|
6
|
+
import { EdrmStorageService } from '../services/edrm-storage.service.js';
|
|
7
|
+
import { FileProvider } from '../models/file.model.js';
|
|
8
|
+
import { validateEdrmStorageConfig } from '../config/environment.example.js';
|
|
9
|
+
import { runEdrmStorageMigrations } from '../migrations/edrm-storage.migration.js';
|
|
10
|
+
async function quickStart() {
|
|
11
|
+
console.log('🚀 Démarrage rapide du module EDRM Storage...\n');
|
|
12
|
+
try {
|
|
13
|
+
// 1. Validation de la configuration
|
|
14
|
+
console.log('📋 Validation de la configuration...');
|
|
15
|
+
const configValidation = validateEdrmStorageConfig();
|
|
16
|
+
if (!configValidation.valid) {
|
|
17
|
+
console.warn('⚠️ Configuration invalide:');
|
|
18
|
+
configValidation.errors.forEach(error => console.warn(` - ${error}`));
|
|
19
|
+
console.warn('\n💡 Configurez vos variables d\'environnement dans le fichier .env');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log('✅ Configuration valide');
|
|
23
|
+
}
|
|
24
|
+
// 2. Initialisation du service
|
|
25
|
+
console.log('\n🔧 Initialisation du service...');
|
|
26
|
+
const storageService = new EdrmStorageService();
|
|
27
|
+
console.log('✅ Service initialisé');
|
|
28
|
+
// 3. Test d'upload
|
|
29
|
+
console.log('\n📤 Test d\'upload...');
|
|
30
|
+
const testData = {
|
|
31
|
+
originalName: 'quick-start-test.pdf',
|
|
32
|
+
mimeType: 'application/pdf',
|
|
33
|
+
size: 1024,
|
|
34
|
+
tenantId: 'quick-start',
|
|
35
|
+
entityName: 'test',
|
|
36
|
+
entityId: 'demo-123'
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const uploadResult = await storageService.initUpload(testData.originalName, testData.mimeType, testData.size, testData.tenantId, testData.entityName, testData.entityId, FileProvider.S3);
|
|
40
|
+
console.log('✅ Upload initialisé avec succès:');
|
|
41
|
+
console.log(` - File ID: ${uploadResult.fileId}`);
|
|
42
|
+
console.log(` - Bucket: ${uploadResult.bucket}`);
|
|
43
|
+
console.log(` - Key: ${uploadResult.key}`);
|
|
44
|
+
console.log(` - URL présignée: ${uploadResult.presignedUrl.substring(0, 50)}...`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.log('⚠️ Test d\'upload échoué (normal si AWS non configuré):');
|
|
48
|
+
console.log(` - Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
|
49
|
+
}
|
|
50
|
+
// 4. Test de listing
|
|
51
|
+
console.log('\n📋 Test de listing...');
|
|
52
|
+
try {
|
|
53
|
+
const listResult = await storageService.listFiles(testData.tenantId, testData.entityName, testData.entityId, undefined, 1, 5);
|
|
54
|
+
console.log('✅ Listing réussi:');
|
|
55
|
+
console.log(` - Total fichiers: ${listResult.total}`);
|
|
56
|
+
console.log(` - Page: ${listResult.page}/${listResult.totalPages}`);
|
|
57
|
+
console.log(` - Fichiers trouvés: ${listResult.files.length}`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.log('⚠️ Test de listing échoué (normal si DB non connectée):');
|
|
61
|
+
console.log(` - Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
|
62
|
+
}
|
|
63
|
+
// 5. Test de génération d'URL de téléchargement
|
|
64
|
+
console.log('\n🔗 Test de génération d\'URL de téléchargement...');
|
|
65
|
+
try {
|
|
66
|
+
// Utiliser un ID fictif pour le test
|
|
67
|
+
const downloadResult = await storageService.getDownloadUrl('507f1f77bcf86cd799439011', // ID MongoDB fictif
|
|
68
|
+
'test-download.pdf', 3600);
|
|
69
|
+
console.log('✅ URL de téléchargement générée:');
|
|
70
|
+
console.log(` - URL: ${downloadResult.url.substring(0, 50)}...`);
|
|
71
|
+
console.log(` - Expire: ${downloadResult.expiresAt}`);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.log('⚠️ Test d\'URL échoué (normal si fichier inexistant):');
|
|
75
|
+
console.log(` - Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
|
76
|
+
}
|
|
77
|
+
// 6. Test des migrations
|
|
78
|
+
console.log('\n🗄️ Test des migrations...');
|
|
79
|
+
try {
|
|
80
|
+
await runEdrmStorageMigrations();
|
|
81
|
+
console.log('✅ Migrations exécutées avec succès');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.log('⚠️ Migrations échouées (normal si DB non connectée):');
|
|
85
|
+
console.log(` - Erreur: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
|
86
|
+
}
|
|
87
|
+
// 7. Résumé
|
|
88
|
+
console.log('\n📊 Résumé du test:');
|
|
89
|
+
console.log('✅ Module EDRM Storage fonctionnel');
|
|
90
|
+
console.log('✅ Service initialisé correctement');
|
|
91
|
+
console.log('✅ API prête à être utilisée');
|
|
92
|
+
if (!configValidation.valid) {
|
|
93
|
+
console.log('\n⚠️ Recommandations:');
|
|
94
|
+
console.log(' - Configurez vos variables d\'environnement AWS');
|
|
95
|
+
console.log(' - Assurez-vous que MongoDB est connecté');
|
|
96
|
+
console.log(' - Vérifiez vos permissions S3');
|
|
97
|
+
}
|
|
98
|
+
console.log('\n🎉 Test terminé avec succès!');
|
|
99
|
+
console.log('\n📚 Prochaines étapes:');
|
|
100
|
+
console.log(' 1. Configurez vos variables d\'environnement');
|
|
101
|
+
console.log(' 2. Intégrez le module dans votre application');
|
|
102
|
+
console.log(' 3. Testez les endpoints REST');
|
|
103
|
+
console.log(' 4. Consultez la documentation pour plus d\'informations');
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('\n❌ Erreur lors du test:', error);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Exécuter le script si appelé directement
|
|
111
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
112
|
+
quickStart();
|
|
113
|
+
}
|
|
114
|
+
export { quickStart };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FileStatus, FileProvider } from '../models/file.model.js';
|
|
2
|
+
export declare class EdrmStorageService {
|
|
3
|
+
private providers;
|
|
4
|
+
private defaultProvider;
|
|
5
|
+
constructor();
|
|
6
|
+
private initializeProviders;
|
|
7
|
+
private getProvider;
|
|
8
|
+
private generateKey;
|
|
9
|
+
private detectFileType;
|
|
10
|
+
initUpload(originalName: string, mimeType: string, size: number, tenantId: string, entityName: string, entityId: string, provider?: FileProvider, metadata?: Record<string, any>, tags?: string[]): Promise<{
|
|
11
|
+
fileId: import("mongoose").Types.ObjectId;
|
|
12
|
+
uploadId: string;
|
|
13
|
+
presignedUrl: string;
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
bucket: string;
|
|
16
|
+
key: string;
|
|
17
|
+
}>;
|
|
18
|
+
completeUpload(fileId: string): Promise<any>;
|
|
19
|
+
getDownloadUrl(fileId: string, filename?: string, expiresIn?: number): Promise<any>;
|
|
20
|
+
deleteFile(fileId: string): Promise<void>;
|
|
21
|
+
listFiles(tenantId?: string, entityName?: string, entityId?: string, status?: FileStatus, page?: number, limit?: number): Promise<{
|
|
22
|
+
files: any[];
|
|
23
|
+
total: number;
|
|
24
|
+
page: number;
|
|
25
|
+
totalPages: number;
|
|
26
|
+
}>;
|
|
27
|
+
getFileById(fileId: string): Promise<any>;
|
|
28
|
+
private emitFileStoredEvent;
|
|
29
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import FileModel, { FileStatus, FileProvider, FileType } from '../models/file.model.js';
|
|
2
|
+
import { S3StorageProvider } from '../providers/s3-storage.provider.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
export class EdrmStorageService {
|
|
6
|
+
providers = new Map();
|
|
7
|
+
defaultProvider = FileProvider.S3;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.initializeProviders();
|
|
10
|
+
}
|
|
11
|
+
initializeProviders() {
|
|
12
|
+
// Initialiser le provider S3 par défaut
|
|
13
|
+
const s3Provider = new S3StorageProvider(process.env.AWS_REGION || 'us-east-1', process.env.AWS_ACCESS_KEY_ID, process.env.AWS_SECRET_ACCESS_KEY);
|
|
14
|
+
this.providers.set(FileProvider.S3, s3Provider);
|
|
15
|
+
}
|
|
16
|
+
getProvider(provider = this.defaultProvider) {
|
|
17
|
+
const storageProvider = this.providers.get(provider);
|
|
18
|
+
if (!storageProvider) {
|
|
19
|
+
throw new Error(`Provider ${provider} non supporté`);
|
|
20
|
+
}
|
|
21
|
+
return storageProvider;
|
|
22
|
+
}
|
|
23
|
+
generateKey(tenantId, entityName, entityId, filename) {
|
|
24
|
+
const timestamp = Date.now();
|
|
25
|
+
const hash = crypto.createHash('md5').update(`${filename}-${timestamp}`).digest('hex');
|
|
26
|
+
const extension = path.extname(filename);
|
|
27
|
+
const nameWithoutExt = path.basename(filename, extension);
|
|
28
|
+
return `${tenantId}/${entityName}/${entityId}/${nameWithoutExt}-${hash}${extension}`;
|
|
29
|
+
}
|
|
30
|
+
detectFileType(mimeType) {
|
|
31
|
+
if (mimeType.startsWith('image/'))
|
|
32
|
+
return FileType.IMAGE;
|
|
33
|
+
if (mimeType.startsWith('video/'))
|
|
34
|
+
return FileType.VIDEO;
|
|
35
|
+
if (mimeType.startsWith('audio/'))
|
|
36
|
+
return FileType.AUDIO;
|
|
37
|
+
if (mimeType.includes('pdf') || mimeType.includes('document') || mimeType.includes('text'))
|
|
38
|
+
return FileType.DOCUMENT;
|
|
39
|
+
if (mimeType.includes('zip') || mimeType.includes('tar') || mimeType.includes('rar'))
|
|
40
|
+
return FileType.ARCHIVE;
|
|
41
|
+
return FileType.OTHER;
|
|
42
|
+
}
|
|
43
|
+
async initUpload(originalName, mimeType, size, tenantId, entityName, entityId, provider = this.defaultProvider, metadata, tags) {
|
|
44
|
+
const bucket = process.env.S3_BUCKET || 'edrm-storage';
|
|
45
|
+
const key = this.generateKey(tenantId, entityName, entityId, originalName);
|
|
46
|
+
const storageProvider = this.getProvider(provider);
|
|
47
|
+
const uploadResponse = await storageProvider.initUpload(bucket, key, mimeType, 3600 // 1 heure
|
|
48
|
+
);
|
|
49
|
+
// Créer l'enregistrement en base
|
|
50
|
+
const fileRecord = new FileModel({
|
|
51
|
+
filename: path.basename(key),
|
|
52
|
+
originalName,
|
|
53
|
+
mimeType,
|
|
54
|
+
size,
|
|
55
|
+
provider,
|
|
56
|
+
bucket,
|
|
57
|
+
key,
|
|
58
|
+
url: uploadResponse.presignedUrl,
|
|
59
|
+
status: FileStatus.PENDING,
|
|
60
|
+
type: this.detectFileType(mimeType),
|
|
61
|
+
metadata,
|
|
62
|
+
tags,
|
|
63
|
+
tenantId,
|
|
64
|
+
entityName,
|
|
65
|
+
entityId,
|
|
66
|
+
uploadedBy: 'system', // À remplacer par l'utilisateur connecté
|
|
67
|
+
accessCount: 0
|
|
68
|
+
});
|
|
69
|
+
await fileRecord.save();
|
|
70
|
+
return {
|
|
71
|
+
fileId: fileRecord._id,
|
|
72
|
+
uploadId: uploadResponse.uploadId,
|
|
73
|
+
presignedUrl: uploadResponse.presignedUrl,
|
|
74
|
+
expiresAt: uploadResponse.expiresAt,
|
|
75
|
+
bucket,
|
|
76
|
+
key
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async completeUpload(fileId) {
|
|
80
|
+
const fileRecord = await FileModel.findById(fileId);
|
|
81
|
+
if (!fileRecord) {
|
|
82
|
+
throw new Error('Fichier non trouvé');
|
|
83
|
+
}
|
|
84
|
+
const storageProvider = this.getProvider(fileRecord.provider);
|
|
85
|
+
try {
|
|
86
|
+
// Vérifier que le fichier existe dans le stockage
|
|
87
|
+
const metadata = await storageProvider.getFileMetadata(fileRecord.bucket, fileRecord.key);
|
|
88
|
+
// Mettre à jour les métadonnées
|
|
89
|
+
fileRecord.status = FileStatus.COMPLETED;
|
|
90
|
+
fileRecord.size = metadata.size;
|
|
91
|
+
fileRecord.etag = metadata.etag;
|
|
92
|
+
fileRecord.checksum = metadata.etag;
|
|
93
|
+
fileRecord.url = `https://${fileRecord.bucket}.s3.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com/${fileRecord.key}`;
|
|
94
|
+
await fileRecord.save();
|
|
95
|
+
// Émettre un événement (à implémenter selon votre système d'événements)
|
|
96
|
+
this.emitFileStoredEvent(fileRecord);
|
|
97
|
+
return fileRecord;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
fileRecord.status = FileStatus.FAILED;
|
|
101
|
+
await fileRecord.save();
|
|
102
|
+
throw new Error(`Erreur lors de la finalisation de l'upload: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async getDownloadUrl(fileId, filename, expiresIn = 3600) {
|
|
106
|
+
const fileRecord = await FileModel.findById(fileId);
|
|
107
|
+
if (!fileRecord) {
|
|
108
|
+
throw new Error('Fichier non trouvé');
|
|
109
|
+
}
|
|
110
|
+
if (fileRecord.status !== FileStatus.COMPLETED) {
|
|
111
|
+
throw new Error('Fichier non encore finalisé');
|
|
112
|
+
}
|
|
113
|
+
const storageProvider = this.getProvider(fileRecord.provider);
|
|
114
|
+
const downloadResponse = await storageProvider.getDownloadUrl(fileRecord.bucket, fileRecord.key, filename || fileRecord.originalName, expiresIn);
|
|
115
|
+
// Mettre à jour les statistiques d'accès
|
|
116
|
+
fileRecord.lastAccessedAt = new Date();
|
|
117
|
+
fileRecord.accessCount = (fileRecord.accessCount || 0) + 1;
|
|
118
|
+
await fileRecord.save();
|
|
119
|
+
return {
|
|
120
|
+
url: downloadResponse.url,
|
|
121
|
+
expiresAt: downloadResponse.expiresAt,
|
|
122
|
+
filename: downloadResponse.filename,
|
|
123
|
+
contentType: downloadResponse.contentType
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async deleteFile(fileId) {
|
|
127
|
+
const fileRecord = await FileModel.findById(fileId);
|
|
128
|
+
if (!fileRecord) {
|
|
129
|
+
throw new Error('Fichier non trouvé');
|
|
130
|
+
}
|
|
131
|
+
const storageProvider = this.getProvider(fileRecord.provider);
|
|
132
|
+
try {
|
|
133
|
+
// Supprimer du stockage
|
|
134
|
+
await storageProvider.deleteFile(fileRecord.bucket, fileRecord.key);
|
|
135
|
+
// Marquer comme supprimé en base
|
|
136
|
+
fileRecord.status = FileStatus.DELETED;
|
|
137
|
+
await fileRecord.save();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new Error(`Erreur lors de la suppression: ${error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async listFiles(tenantId, entityName, entityId, status, page = 1, limit = 20) {
|
|
144
|
+
const query = {};
|
|
145
|
+
if (tenantId)
|
|
146
|
+
query.tenantId = tenantId;
|
|
147
|
+
if (entityName)
|
|
148
|
+
query.entityName = entityName;
|
|
149
|
+
if (entityId)
|
|
150
|
+
query.entityId = entityId;
|
|
151
|
+
if (status)
|
|
152
|
+
query.status = status;
|
|
153
|
+
const skip = (page - 1) * limit;
|
|
154
|
+
const [files, total] = await Promise.all([
|
|
155
|
+
FileModel.find(query)
|
|
156
|
+
.sort({ createdAt: -1 })
|
|
157
|
+
.skip(skip)
|
|
158
|
+
.limit(limit)
|
|
159
|
+
.exec(),
|
|
160
|
+
FileModel.countDocuments(query)
|
|
161
|
+
]);
|
|
162
|
+
return {
|
|
163
|
+
files,
|
|
164
|
+
total,
|
|
165
|
+
page,
|
|
166
|
+
totalPages: Math.ceil(total / limit)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async getFileById(fileId) {
|
|
170
|
+
const fileRecord = await FileModel.findById(fileId);
|
|
171
|
+
if (!fileRecord) {
|
|
172
|
+
throw new Error('Fichier non trouvé');
|
|
173
|
+
}
|
|
174
|
+
return fileRecord;
|
|
175
|
+
}
|
|
176
|
+
emitFileStoredEvent(fileRecord) {
|
|
177
|
+
// À implémenter selon votre système d'événements
|
|
178
|
+
// Exemple avec un système d'événements simple
|
|
179
|
+
console.log('FileStored event emitted:', {
|
|
180
|
+
fileId: fileRecord._id,
|
|
181
|
+
filename: fileRecord.filename,
|
|
182
|
+
size: fileRecord.size,
|
|
183
|
+
tenantId: fileRecord.tenantId,
|
|
184
|
+
entityName: fileRecord.entityName,
|
|
185
|
+
entityId: fileRecord.entityId
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { EdrmStorageService } from '../services/edrm-storage.service.js';
|
|
2
|
+
import { FileProvider, FileStatus } from '../models/file.model.js';
|
|
3
|
+
describe('EdrmStorageService', () => {
|
|
4
|
+
let storageService;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
storageService = new EdrmStorageService();
|
|
7
|
+
});
|
|
8
|
+
describe('initUpload', () => {
|
|
9
|
+
it('devrait initialiser un upload avec succès', async () => {
|
|
10
|
+
const mockData = {
|
|
11
|
+
originalName: 'test.pdf',
|
|
12
|
+
mimeType: 'application/pdf',
|
|
13
|
+
size: 1024000,
|
|
14
|
+
tenantId: 'test-tenant',
|
|
15
|
+
entityName: 'test-entity',
|
|
16
|
+
entityId: 'test-id'
|
|
17
|
+
};
|
|
18
|
+
// Mock du provider S3
|
|
19
|
+
const mockProvider = {
|
|
20
|
+
initUpload: jest.fn().mockResolvedValue({
|
|
21
|
+
uploadId: 'test-upload-id',
|
|
22
|
+
presignedUrl: 'https://test-url.com',
|
|
23
|
+
expiresAt: new Date(),
|
|
24
|
+
bucket: 'test-bucket',
|
|
25
|
+
key: 'test-key'
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
// Mock du modèle File
|
|
29
|
+
const mockFileModel = {
|
|
30
|
+
save: jest.fn().mockResolvedValue({
|
|
31
|
+
_id: 'test-file-id',
|
|
32
|
+
...mockData
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
// Injecter les mocks
|
|
36
|
+
storageService.providers.set(FileProvider.S3, mockProvider);
|
|
37
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => mockFileModel);
|
|
38
|
+
const result = await storageService.initUpload(mockData.originalName, mockData.mimeType, mockData.size, mockData.tenantId, mockData.entityName, mockData.entityId);
|
|
39
|
+
expect(result).toBeDefined();
|
|
40
|
+
expect(result.fileId).toBe('test-file-id');
|
|
41
|
+
expect(mockProvider.initUpload).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
it('devrait rejeter avec une erreur si les paramètres sont manquants', async () => {
|
|
44
|
+
await expect(storageService.initUpload('', 'application/pdf', 1024000, 'test-tenant', 'test-entity', 'test-id')).rejects.toThrow();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('completeUpload', () => {
|
|
48
|
+
it('devrait finaliser un upload avec succès', async () => {
|
|
49
|
+
const mockFileRecord = {
|
|
50
|
+
_id: 'test-file-id',
|
|
51
|
+
status: FileStatus.PENDING,
|
|
52
|
+
bucket: 'test-bucket',
|
|
53
|
+
key: 'test-key',
|
|
54
|
+
provider: FileProvider.S3,
|
|
55
|
+
save: jest.fn().mockResolvedValue(true)
|
|
56
|
+
};
|
|
57
|
+
const mockProvider = {
|
|
58
|
+
getFileMetadata: jest.fn().mockResolvedValue({
|
|
59
|
+
size: 1024000,
|
|
60
|
+
etag: 'test-etag',
|
|
61
|
+
lastModified: new Date(),
|
|
62
|
+
contentType: 'application/pdf'
|
|
63
|
+
})
|
|
64
|
+
};
|
|
65
|
+
// Mock du modèle File
|
|
66
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => ({
|
|
67
|
+
findById: jest.fn().mockResolvedValue(mockFileRecord)
|
|
68
|
+
}));
|
|
69
|
+
storageService.providers.set(FileProvider.S3, mockProvider);
|
|
70
|
+
const result = await storageService.completeUpload('test-file-id');
|
|
71
|
+
expect(result.status).toBe(FileStatus.COMPLETED);
|
|
72
|
+
expect(mockProvider.getFileMetadata).toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
it('devrait rejeter si le fichier n\'existe pas', async () => {
|
|
75
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => ({
|
|
76
|
+
findById: jest.fn().mockResolvedValue(null)
|
|
77
|
+
}));
|
|
78
|
+
await expect(storageService.completeUpload('non-existent-id')).rejects.toThrow('Fichier non trouvé');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('getDownloadUrl', () => {
|
|
82
|
+
it('devrait générer une URL de téléchargement', async () => {
|
|
83
|
+
const mockFileRecord = {
|
|
84
|
+
_id: 'test-file-id',
|
|
85
|
+
status: FileStatus.COMPLETED,
|
|
86
|
+
bucket: 'test-bucket',
|
|
87
|
+
key: 'test-key',
|
|
88
|
+
provider: FileProvider.S3,
|
|
89
|
+
originalName: 'test.pdf',
|
|
90
|
+
accessCount: 0,
|
|
91
|
+
save: jest.fn().mockResolvedValue(true)
|
|
92
|
+
};
|
|
93
|
+
const mockProvider = {
|
|
94
|
+
getDownloadUrl: jest.fn().mockResolvedValue({
|
|
95
|
+
url: 'https://download-url.com',
|
|
96
|
+
expiresAt: new Date(),
|
|
97
|
+
filename: 'test.pdf',
|
|
98
|
+
contentType: 'application/pdf'
|
|
99
|
+
})
|
|
100
|
+
};
|
|
101
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => ({
|
|
102
|
+
findById: jest.fn().mockResolvedValue(mockFileRecord)
|
|
103
|
+
}));
|
|
104
|
+
storageService.providers.set(FileProvider.S3, mockProvider);
|
|
105
|
+
const result = await storageService.getDownloadUrl('test-file-id');
|
|
106
|
+
expect(result.url).toBeDefined();
|
|
107
|
+
expect(mockProvider.getDownloadUrl).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it('devrait rejeter si le fichier n\'est pas finalisé', async () => {
|
|
110
|
+
const mockFileRecord = {
|
|
111
|
+
_id: 'test-file-id',
|
|
112
|
+
status: FileStatus.PENDING
|
|
113
|
+
};
|
|
114
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => ({
|
|
115
|
+
findById: jest.fn().mockResolvedValue(mockFileRecord)
|
|
116
|
+
}));
|
|
117
|
+
await expect(storageService.getDownloadUrl('test-file-id')).rejects.toThrow('Fichier non encore finalisé');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('listFiles', () => {
|
|
121
|
+
it('devrait lister les fichiers avec pagination', async () => {
|
|
122
|
+
const mockFiles = [
|
|
123
|
+
{ _id: 'file-1', filename: 'test1.pdf' },
|
|
124
|
+
{ _id: 'file-2', filename: 'test2.pdf' }
|
|
125
|
+
];
|
|
126
|
+
jest.spyOn(require('../models/file.model.js'), 'default').mockImplementation(() => ({
|
|
127
|
+
find: jest.fn().mockReturnValue({
|
|
128
|
+
sort: jest.fn().mockReturnValue({
|
|
129
|
+
skip: jest.fn().mockReturnValue({
|
|
130
|
+
limit: jest.fn().mockReturnValue({
|
|
131
|
+
exec: jest.fn().mockResolvedValue(mockFiles)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
}),
|
|
136
|
+
countDocuments: jest.fn().mockResolvedValue(2)
|
|
137
|
+
}));
|
|
138
|
+
const result = await storageService.listFiles('test-tenant', 'test-entity', 'test-id');
|
|
139
|
+
expect(result.files).toHaveLength(2);
|
|
140
|
+
expect(result.total).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|