@programisto/edrm-storage 1.0.3 → 1.0.5

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 CHANGED
@@ -21,7 +21,7 @@ The Endurance Framework is a highly modular and scalable Node.js project templat
21
21
 
22
22
  ### Installation
23
23
 
24
- 1. Install our CLI:
24
+ 1. Install our CLI (optionnal):
25
25
 
26
26
  ```sh
27
27
  npm install -g endurance
@@ -7,6 +7,12 @@ export declare class S3StorageProvider implements StorageProvider {
7
7
  constructor(region: string, accessKeyId?: string, secretAccessKey?: string);
8
8
  initUpload(bucket: string, key: string, contentType: string, expiresIn?: number): Promise<UploadInitResponse>;
9
9
  getFileMetadata(bucket: string, key: string): Promise<FileMetadata>;
10
+ /**
11
+ * Encode un nom de fichier pour l'en-tête Content-Disposition selon RFC 6266
12
+ * Gère les caractères spéciaux (accents, etc.) en utilisant l'encodage UTF-8
13
+ * AWS S3 nécessite un encodage strict pour éviter l'erreur "Header value cannot be represented using ISO-8859-1"
14
+ */
15
+ private encodeContentDispositionFilename;
10
16
  getDownloadUrl(bucket: string, key: string, filename?: string, expiresIn?: number): Promise<DownloadUrlResponse>;
11
17
  deleteFile(bucket: string, key: string): Promise<void>;
12
18
  copyFile(sourceBucket: string, sourceKey: string, destBucket: string, destKey: string): Promise<void>;
@@ -48,11 +48,34 @@ export class S3StorageProvider {
48
48
  metadata: response.Metadata
49
49
  };
50
50
  }
51
+ /**
52
+ * Encode un nom de fichier pour l'en-tête Content-Disposition selon RFC 6266
53
+ * Gère les caractères spéciaux (accents, etc.) en utilisant l'encodage UTF-8
54
+ * AWS S3 nécessite un encodage strict pour éviter l'erreur "Header value cannot be represented using ISO-8859-1"
55
+ */
56
+ encodeContentDispositionFilename(filename) {
57
+ // Vérifier si le nom de fichier contient des caractères non-ASCII
58
+ const hasNonAscii = /[^\x20-\x7E]/.test(filename);
59
+ if (hasNonAscii) {
60
+ // Pour les noms avec caractères non-ASCII, utiliser uniquement filename* avec UTF-8
61
+ // Cela évite les problèmes avec S3 qui rejette les caractères non-ASCII dans filename
62
+ const encodedFilename = encodeURIComponent(filename)
63
+ .replace(/'/g, '%27')
64
+ .replace(/\(/g, '%28')
65
+ .replace(/\)/g, '%29');
66
+ // Format RFC 6266 : utiliser uniquement filename* pour éviter les erreurs S3
67
+ return `attachment; filename*=UTF-8''${encodedFilename}`;
68
+ }
69
+ else {
70
+ // Pour les noms ASCII uniquement, utiliser le format simple
71
+ return `attachment; filename="${filename}"`;
72
+ }
73
+ }
51
74
  async getDownloadUrl(bucket, key, filename, expiresIn = 3600) {
52
75
  const command = new GetObjectCommand({
53
76
  Bucket: bucket,
54
77
  Key: key,
55
- ResponseContentDisposition: filename ? `attachment; filename="${filename}"` : undefined
78
+ ResponseContentDisposition: filename ? this.encodeContentDispositionFilename(filename) : undefined
56
79
  });
57
80
  const url = await getSignedUrl(this.client, command, {
58
81
  expiresIn
@@ -14,7 +14,60 @@ class EdrmStorageRouter extends EnduranceRouter {
14
14
  const userOptions = {
15
15
  requireAuth: true
16
16
  };
17
- // Routes publiques pour l'initialisation d'upload
17
+ /**
18
+ * @swagger
19
+ * /files/init:
20
+ * post:
21
+ * summary: Initialiser un upload de fichier
22
+ * description: Crée un enregistrement de fichier et retourne une URL signée pour commencer l’upload. Authentification utilisateur requise.
23
+ * tags: [Stockage]
24
+ * requestBody:
25
+ * required: true
26
+ * content:
27
+ * application/json:
28
+ * schema:
29
+ * type: object
30
+ * required: [originalName, mimeType, size, tenantId, entityName, entityId]
31
+ * properties:
32
+ * originalName:
33
+ * type: string
34
+ * description: Nom d’origine du fichier
35
+ * mimeType:
36
+ * type: string
37
+ * description: Type MIME du fichier
38
+ * size:
39
+ * type: integer
40
+ * description: Taille du fichier en octets
41
+ * tenantId:
42
+ * type: string
43
+ * description: Identifiant du tenant
44
+ * entityName:
45
+ * type: string
46
+ * description: Entité métier associée (ex. "invoice")
47
+ * entityId:
48
+ * type: string
49
+ * description: Identifiant de l’entité métier associée
50
+ * provider:
51
+ * type: string
52
+ * enum: [S3]
53
+ * default: S3
54
+ * description: Provider de stockage
55
+ * metadata:
56
+ * type: object
57
+ * description: Métadonnées supplémentaires
58
+ * tags:
59
+ * type: array
60
+ * items:
61
+ * type: string
62
+ * description: Liste de tags libres
63
+ * responses:
64
+ * 200:
65
+ * description: URL signée et informations d’initialisation
66
+ * 400:
67
+ * description: Paramètres manquants ou invalides
68
+ * 500:
69
+ * description: Erreur serveur
70
+ */
18
71
  this.post('/files/init', userOptions, async (req, res) => {
19
72
  try {
20
73
  const { originalName, mimeType, size, tenantId, entityName, entityId, provider = 'S3', metadata, tags } = req.body;
@@ -38,7 +91,26 @@ class EdrmStorageRouter extends EnduranceRouter {
38
91
  });
39
92
  }
40
93
  });
41
- // Finaliser un upload
94
+ /**
95
+ * @swagger
96
+ * /files/{fileId}/complete:
97
+ * post:
98
+ * summary: Finaliser un upload
99
+ * description: Marque l’upload comme terminé et met à jour le statut du fichier. Authentification utilisateur requise.
100
+ * tags: [Stockage]
101
+ * parameters:
102
+ * - in: path
103
+ * name: fileId
104
+ * required: true
105
+ * schema:
106
+ * type: string
107
+ * description: Identifiant du fichier
108
+ * responses:
109
+ * 200:
110
+ * description: Upload finalisé avec succès
111
+ * 500:
112
+ * description: Erreur serveur
113
+ */
42
114
  this.post('/files/:fileId/complete', userOptions, async (req, res) => {
43
115
  try {
44
116
  const { fileId } = req.params;
@@ -56,7 +128,37 @@ class EdrmStorageRouter extends EnduranceRouter {
56
128
  });
57
129
  }
58
130
  });
59
- // Obtenir une URL de téléchargement
131
+ /**
132
+ * @swagger
133
+ * /files/{fileId}/download:
134
+ * get:
135
+ * summary: Obtenir une URL de téléchargement
136
+ * description: Retourne une URL signée pour télécharger le fichier. Authentification utilisateur requise.
137
+ * tags: [Stockage]
138
+ * parameters:
139
+ * - in: path
140
+ * name: fileId
141
+ * required: true
142
+ * schema:
143
+ * type: string
144
+ * description: Identifiant du fichier
145
+ * - in: query
146
+ * name: filename
147
+ * schema:
148
+ * type: string
149
+ * description: Nom de fichier à proposer au téléchargement
150
+ * - in: query
151
+ * name: expiresIn
152
+ * schema:
153
+ * type: integer
154
+ * default: 3600
155
+ * description: Durée de validité du lien en secondes
156
+ * responses:
157
+ * 200:
158
+ * description: URL de téléchargement générée
159
+ * 500:
160
+ * description: Erreur serveur
161
+ */
60
162
  this.get('/files/:fileId/download', userOptions, async (req, res) => {
61
163
  try {
62
164
  const { fileId } = req.params;
@@ -75,7 +177,26 @@ class EdrmStorageRouter extends EnduranceRouter {
75
177
  });
76
178
  }
77
179
  });
78
- // Supprimer un fichier
180
+ /**
181
+ * @swagger
182
+ * /files/{fileId}:
183
+ * delete:
184
+ * summary: Supprimer un fichier
185
+ * description: Supprime un fichier et ses métadonnées. Authentification et permission Admin_ManageFiles requises.
186
+ * tags: [Stockage]
187
+ * parameters:
188
+ * - in: path
189
+ * name: fileId
190
+ * required: true
191
+ * schema:
192
+ * type: string
193
+ * description: Identifiant du fichier
194
+ * responses:
195
+ * 200:
196
+ * description: Fichier supprimé
197
+ * 500:
198
+ * description: Erreur serveur
199
+ */
79
200
  this.delete('/files/:fileId', adminOptions, async (req, res) => {
80
201
  try {
81
202
  const { fileId } = req.params;
@@ -93,7 +214,53 @@ class EdrmStorageRouter extends EnduranceRouter {
93
214
  });
94
215
  }
95
216
  });
96
- // Lister les fichiers
217
+ /**
218
+ * @swagger
219
+ * /files:
220
+ * get:
221
+ * summary: Lister les fichiers
222
+ * description: Retourne une liste paginée des fichiers avec filtres. Authentification et permission Admin_ManageFiles requises.
223
+ * tags: [Stockage]
224
+ * parameters:
225
+ * - in: query
226
+ * name: tenantId
227
+ * schema:
228
+ * type: string
229
+ * description: Filtrer par tenant
230
+ * - in: query
231
+ * name: entityName
232
+ * schema:
233
+ * type: string
234
+ * description: Filtrer par entité
235
+ * - in: query
236
+ * name: entityId
237
+ * schema:
238
+ * type: string
239
+ * description: Filtrer par identifiant d’entité
240
+ * - in: query
241
+ * name: status
242
+ * schema:
243
+ * type: string
244
+ * enum: [PENDING, COMPLETED, FAILED]
245
+ * description: Filtrer par statut de fichier
246
+ * - in: query
247
+ * name: page
248
+ * schema:
249
+ * type: integer
250
+ * default: 1
251
+ * description: Numéro de page
252
+ * - in: query
253
+ * name: limit
254
+ * schema:
255
+ * type: integer
256
+ * default: 20
257
+ * description: Nombre d’éléments par page
258
+ * responses:
259
+ * 200:
260
+ * description: Liste paginée de fichiers
261
+ * 500:
262
+ * description: Erreur serveur
263
+ */
97
264
  this.get('/files', adminOptions, async (req, res) => {
98
265
  try {
99
266
  const { tenantId, entityName, entityId, status, page = 1, limit = 20 } = req.query;
@@ -111,7 +278,26 @@ class EdrmStorageRouter extends EnduranceRouter {
111
278
  });
112
279
  }
113
280
  });
114
- // Obtenir les détails d'un fichier
281
+ /**
282
+ * @swagger
283
+ * /files/{fileId}:
284
+ * get:
285
+ * summary: Récupérer un fichier par ID
286
+ * description: Retourne les métadonnées d’un fichier. Authentification utilisateur requise.
287
+ * tags: [Stockage]
288
+ * parameters:
289
+ * - in: path
290
+ * name: fileId
291
+ * required: true
292
+ * schema:
293
+ * type: string
294
+ * description: Identifiant du fichier
295
+ * responses:
296
+ * 200:
297
+ * description: Détails du fichier
298
+ * 500:
299
+ * description: Erreur serveur
300
+ */
115
301
  this.get('/files/:fileId', userOptions, async (req, res) => {
116
302
  try {
117
303
  const { fileId } = req.params;
@@ -129,7 +315,26 @@ class EdrmStorageRouter extends EnduranceRouter {
129
315
  });
130
316
  }
131
317
  });
132
- // Route de callback pour les événements S3 (optionnel)
318
+ /**
319
+ * @swagger
320
+ * /files/s3-callback:
321
+ * post:
322
+ * summary: Callback d’événements S3
323
+ * description: Réception et traitement des webhooks S3. Authentification non requise.
324
+ * tags: [Stockage]
325
+ * requestBody:
326
+ * required: false
327
+ * content:
328
+ * application/json:
329
+ * schema:
330
+ * type: object
331
+ * description: Payload du webhook S3
332
+ * responses:
333
+ * 200:
334
+ * description: Callback traité
335
+ * 500:
336
+ * description: Erreur serveur
337
+ */
133
338
  this.post('/files/s3-callback', { requireAuth: false, permissions: [] }, async (req, res) => {
134
339
  try {
135
340
  // Cette route peut être utilisée pour recevoir des webhooks S3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-storage",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },