b2b-platform-utils 1.1.6 → 1.1.7
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/fileSystem.js +287 -29
- package/package.json +1 -1
package/fileSystem.js
CHANGED
|
@@ -1,40 +1,298 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @module fileSystem
|
|
5
|
+
* @description
|
|
6
|
+
* Synchronous file and media-related helpers.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Safe read/write/remove operations on local filesystem.
|
|
10
|
+
* - Basic MIME / extension resolution.
|
|
11
|
+
* - Utility to build absolute media URLs based on service context.
|
|
12
|
+
*
|
|
13
|
+
* Notes:
|
|
14
|
+
* - All filesystem paths are resolved against process.cwd() unless already absolute.
|
|
15
|
+
* - Synchronous I/O will block the Node.js event loop. This is acceptable for:
|
|
16
|
+
* - service bootstrap
|
|
17
|
+
* - low-frequency admin/debug tasks
|
|
18
|
+
* - cron/workers with low concurrency
|
|
19
|
+
* It is NOT recommended in hot request/response paths.
|
|
20
|
+
*/
|
|
4
21
|
|
|
5
22
|
const fs = require('fs');
|
|
6
23
|
const path = require('path');
|
|
24
|
+
const { warningNote } = require('./logger');
|
|
25
|
+
const { getValue } = require('./localCache');
|
|
7
26
|
|
|
8
|
-
|
|
27
|
+
// -----------------------------------------------------------------------------
|
|
28
|
+
// Internal helpers
|
|
29
|
+
// -----------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an absolute filesystem path. If the provided path is relative,
|
|
33
|
+
* it will be resolved against process.cwd().
|
|
34
|
+
*
|
|
35
|
+
* @param {string} fileName - Absolute or relative path.
|
|
36
|
+
* @returns {string} Absolute path.
|
|
37
|
+
*/
|
|
9
38
|
function resolvePath(fileName) {
|
|
10
|
-
return path.isAbsolute(fileName)
|
|
39
|
+
return path.isAbsolute(fileName)
|
|
40
|
+
? fileName
|
|
41
|
+
: path.resolve(process.cwd(), fileName);
|
|
11
42
|
}
|
|
12
43
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Ensure the parent directory for a given absolute file path exists.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} absolutePath
|
|
48
|
+
*/
|
|
49
|
+
function ensureDirForFile(absolutePath) {
|
|
50
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize file extension:
|
|
55
|
+
* - strips query string / hash fragments
|
|
56
|
+
* - lowercases the extension
|
|
57
|
+
*
|
|
58
|
+
* @param {string} filename
|
|
59
|
+
* @returns {string} extension without dot, e.g. "png"
|
|
60
|
+
*/
|
|
61
|
+
function extractCleanExtension(filename) {
|
|
62
|
+
// remove query/hash if passed like "avatar.png?ts=123" or "file.pdf#v2"
|
|
63
|
+
const cleanName = filename.split('?')[0].split('#')[0];
|
|
64
|
+
const parts = cleanName.split('.');
|
|
65
|
+
if (parts.length < 2) return '';
|
|
66
|
+
return parts.pop().toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// -----------------------------------------------------------------------------
|
|
70
|
+
// Public API: Filesystem I/O
|
|
71
|
+
// -----------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save a plain JS value as JSON into a file (overwrites if it already exists).
|
|
75
|
+
* Ensures parent directory exists.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} fileName - Target file path (absolute or relative).
|
|
78
|
+
* @param {any} data - Serializable data.
|
|
79
|
+
* @param {Object} [options]
|
|
80
|
+
* @param {number} [options.spaces=0] - JSON indentation level, e.g. 2 for pretty debug output.
|
|
81
|
+
*/
|
|
82
|
+
function saveFile(fileName, data, options = {}) {
|
|
83
|
+
const absPath = resolvePath(fileName);
|
|
84
|
+
ensureDirForFile(absPath);
|
|
85
|
+
|
|
86
|
+
const spaces = Number.isInteger(options.spaces) ? options.spaces : 0;
|
|
87
|
+
const json = JSON.stringify(data, null, spaces);
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(absPath, json, { encoding: 'utf-8' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read file content as UTF-8 text.
|
|
94
|
+
* This will throw if the file is missing or unreadable.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} fileName
|
|
97
|
+
* @returns {string} File contents.
|
|
98
|
+
*/
|
|
99
|
+
function readFile(fileName) {
|
|
100
|
+
const absPath = resolvePath(fileName);
|
|
101
|
+
return fs.readFileSync(absPath, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read JSON file and parse it.
|
|
106
|
+
* Returns null if the file does not exist.
|
|
107
|
+
* Will throw if file exists but content is not valid JSON.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} fileName
|
|
110
|
+
* @returns {any|null}
|
|
111
|
+
*/
|
|
112
|
+
function readJsonFile(fileName) {
|
|
113
|
+
const absPath = resolvePath(fileName);
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(absPath)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const raw = fs.readFileSync(absPath, 'utf-8');
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove a file from the filesystem if it exists.
|
|
125
|
+
* Safe no-op if file is already missing.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} fileName
|
|
128
|
+
*/
|
|
129
|
+
function removeFile(fileName) {
|
|
130
|
+
const absPath = resolvePath(fileName);
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(absPath)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fs.unlinkSync(absPath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -----------------------------------------------------------------------------
|
|
140
|
+
// Public API: MIME / extension helpers
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map known MIME types to file extensions.
|
|
145
|
+
* Unknown types return extension = null and also return an error message.
|
|
146
|
+
* A warning is logged for visibility.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} mimetype - e.g. "image/png", "application/pdf".
|
|
149
|
+
* @returns {{ extension: string|null, error: string }}
|
|
150
|
+
*/
|
|
151
|
+
function getImageExtension(mimetype) {
|
|
152
|
+
let extension = null;
|
|
153
|
+
let error = '';
|
|
154
|
+
|
|
155
|
+
switch (mimetype) {
|
|
156
|
+
case 'image/png':
|
|
157
|
+
extension = 'png';
|
|
158
|
+
break;
|
|
159
|
+
case 'image/jpeg':
|
|
160
|
+
extension = 'jpeg';
|
|
161
|
+
break;
|
|
162
|
+
case 'image/jpg':
|
|
163
|
+
extension = 'jpg';
|
|
164
|
+
break;
|
|
165
|
+
case 'image/svg+xml':
|
|
166
|
+
extension = 'svg';
|
|
167
|
+
break;
|
|
168
|
+
case 'image/webp':
|
|
169
|
+
extension = 'webp';
|
|
170
|
+
break;
|
|
171
|
+
case 'image/gif':
|
|
172
|
+
extension = 'gif';
|
|
173
|
+
break;
|
|
174
|
+
case 'image/vnd.microsoft.icon':
|
|
175
|
+
extension = 'ico';
|
|
176
|
+
break;
|
|
177
|
+
case 'font/ttf':
|
|
178
|
+
extension = 'ttf';
|
|
179
|
+
break;
|
|
180
|
+
case 'text/plain':
|
|
181
|
+
extension = 'txt';
|
|
182
|
+
break;
|
|
183
|
+
case 'text/csv':
|
|
184
|
+
extension = 'csv';
|
|
185
|
+
break;
|
|
186
|
+
case 'application/pdf':
|
|
187
|
+
extension = 'pdf';
|
|
188
|
+
break;
|
|
189
|
+
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
|
190
|
+
extension = 'xlsx';
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
error = `Undefined file mimetype: ${mimetype}`;
|
|
194
|
+
extension = null;
|
|
195
|
+
|
|
196
|
+
// Emit a non-fatal warning. Service can still decide what to do.
|
|
197
|
+
if (typeof warningNote === 'function') {
|
|
198
|
+
warningNote(error);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { extension, error };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Infer MIME type from file extension.
|
|
208
|
+
* If extension is not recognized, returns an empty string.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} filename - Full filename, may contain query/hash.
|
|
211
|
+
* @returns {string} MIME type string or "" if unknown.
|
|
212
|
+
*/
|
|
213
|
+
function getMimeTypeByFileExtension(filename) {
|
|
214
|
+
const ext = extractCleanExtension(filename);
|
|
215
|
+
|
|
216
|
+
switch (ext) {
|
|
217
|
+
case 'png':
|
|
218
|
+
return 'image/png';
|
|
219
|
+
case 'gif':
|
|
220
|
+
return 'image/gif';
|
|
221
|
+
case 'jpeg':
|
|
222
|
+
case 'jpg':
|
|
223
|
+
return 'image/jpeg';
|
|
224
|
+
case 'webp':
|
|
225
|
+
return 'image/webp';
|
|
226
|
+
case 'ico':
|
|
227
|
+
return 'image/vnd.microsoft.icon';
|
|
228
|
+
case 'svg':
|
|
229
|
+
return 'image/svg+xml';
|
|
230
|
+
case 'csv':
|
|
231
|
+
return 'text/csv';
|
|
232
|
+
case 'txt':
|
|
233
|
+
return 'text/plain';
|
|
234
|
+
case 'xml':
|
|
235
|
+
return 'application/xml';
|
|
236
|
+
case 'pdf':
|
|
237
|
+
return 'application/pdf';
|
|
238
|
+
case 'xls':
|
|
239
|
+
return 'application/vnd.ms-excel';
|
|
240
|
+
case 'xlsx':
|
|
241
|
+
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
242
|
+
case 'doc':
|
|
243
|
+
return 'application/msword';
|
|
244
|
+
case 'docx':
|
|
245
|
+
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
246
|
+
case 'mp4':
|
|
247
|
+
return 'video/mp4';
|
|
248
|
+
default:
|
|
249
|
+
return '';
|
|
39
250
|
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -----------------------------------------------------------------------------
|
|
254
|
+
// Public API: URL builders
|
|
255
|
+
// -----------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build absolute URL for static/media resource exposed by the platform.
|
|
259
|
+
* Uses runtime config from localCache.
|
|
260
|
+
*
|
|
261
|
+
* Example:
|
|
262
|
+
* baseApiURL = "https://cdn.example.com"
|
|
263
|
+
* service = "avatar"
|
|
264
|
+
* instance = "user123"
|
|
265
|
+
* fileName = "profile.png"
|
|
266
|
+
*
|
|
267
|
+
* Result:
|
|
268
|
+
* "https://cdn.example.com/file/avatar/user123/profile.png"
|
|
269
|
+
*
|
|
270
|
+
* @param {string} service - Logical service name, e.g. "avatar", "cms", "game".
|
|
271
|
+
* @param {string} instance - Instance or tenant identifier.
|
|
272
|
+
* @param {string} fileName - File name including extension.
|
|
273
|
+
* @returns {string} Absolute URL.
|
|
274
|
+
*/
|
|
275
|
+
function getAbsoluteMediaResourceUrl(service, instance, fileName) {
|
|
276
|
+
const baseUrl = getValue('baseApiURL') || '';
|
|
277
|
+
return `${baseUrl}/file/${service}/${instance}/${fileName}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -----------------------------------------------------------------------------
|
|
281
|
+
// Exports
|
|
282
|
+
// -----------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
// Filesystem I/O
|
|
286
|
+
resolvePath,
|
|
287
|
+
saveFile,
|
|
288
|
+
readFile,
|
|
289
|
+
readJsonFile,
|
|
290
|
+
removeFile,
|
|
291
|
+
|
|
292
|
+
// MIME helpers
|
|
293
|
+
getImageExtension,
|
|
294
|
+
getMimeTypeByFileExtension,
|
|
295
|
+
|
|
296
|
+
// URL helpers
|
|
297
|
+
getAbsoluteMediaResourceUrl
|
|
40
298
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "b2b-platform-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Shared utilities for Node.js microservices: errors map, local cache, logger, numbers, dates, filesystem, media optimization, paginator, slugger, crypto wrapper, sanitize HTML, sorting.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "KingSizer",
|