fs-object-storage 1.0.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/.github/workflows/npm-publish.yml +41 -0
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/docker-compose.yml +24 -0
- package/package.json +45 -0
- package/quick-test.js +36 -0
- package/samples/fs-minio-test.js +135 -0
- package/samples/memfs-sample.js +44 -0
- package/samples/minio-connection-test.js +66 -0
- package/samples/minio-sample.js +64 -0
- package/src/index.d.ts +98 -0
- package/src/index.js +17 -0
- package/src/lib/ErrorHandler.js +149 -0
- package/src/lib/FsMinioClient.js +480 -0
- package/src/lib/PathConverter.js +209 -0
- package/src/lib/StreamConverter.js +281 -0
- package/test-package.json +9 -0
- package/tests/unit/ErrorHandler.test.js +117 -0
- package/tests/unit/PathConverter.test.js +224 -0
- package/tests/unit/StreamConverter.test.js +267 -0
- package/unit-tests.js +101 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// FsMinioClient.js - Main fs-compatible client for MinIO/S3 operations
|
|
2
|
+
|
|
3
|
+
import { Client as MinioClient } from 'minio';
|
|
4
|
+
import PathConverter from './PathConverter.js';
|
|
5
|
+
import StreamConverter from './StreamConverter.js';
|
|
6
|
+
import ErrorHandler from './ErrorHandler.js';
|
|
7
|
+
|
|
8
|
+
class FsMinioClient {
|
|
9
|
+
/**
|
|
10
|
+
* Create FsMinioClient instance
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @param {string} options.endpoint - MinIO endpoint (e.g., 'localhost:9000')
|
|
13
|
+
* @param {string} options.accessKey - Access key
|
|
14
|
+
* @param {string} options.secretKey - Secret key
|
|
15
|
+
* @param {string} options.bucket - Bucket name
|
|
16
|
+
* @param {boolean} [options.useSSL=false] - Use SSL
|
|
17
|
+
* @param {string} [options.region='us-east-1'] - Region
|
|
18
|
+
* @param {string} [options.prefix=''] - Key prefix for all operations
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
// Validate required options
|
|
22
|
+
if (!options.endpoint) throw new Error('endpoint is required');
|
|
23
|
+
if (!options.accessKey) throw new Error('accessKey is required');
|
|
24
|
+
if (!options.secretKey) throw new Error('secretKey is required');
|
|
25
|
+
if (!options.bucket) throw new Error('bucket is required');
|
|
26
|
+
|
|
27
|
+
// Initialize MinIO client
|
|
28
|
+
this.minioClient = new MinioClient({
|
|
29
|
+
endPoint: options.endpoint.split(':')[0],
|
|
30
|
+
port: parseInt(options.endpoint.split(':')[1]) || (options.useSSL ? 443 : 80),
|
|
31
|
+
useSSL: options.useSSL || false,
|
|
32
|
+
accessKey: options.accessKey,
|
|
33
|
+
secretKey: options.secretKey,
|
|
34
|
+
region: options.region || 'us-east-1'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Initialize path converter
|
|
38
|
+
this.pathConverter = new PathConverter({
|
|
39
|
+
bucket: options.bucket,
|
|
40
|
+
prefix: options.prefix
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.bucket = options.bucket;
|
|
44
|
+
this._initialized = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize client (create bucket if it doesn't exist)
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async initialize() {
|
|
52
|
+
if (this._initialized) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const bucketExists = await this.minioClient.bucketExists(this.bucket);
|
|
56
|
+
if (!bucketExists) {
|
|
57
|
+
await this.minioClient.makeBucket(this.bucket);
|
|
58
|
+
}
|
|
59
|
+
this._initialized = true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw ErrorHandler.convertError(error, null, 'initialize');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read file content
|
|
67
|
+
* @param {string} filePath - File path
|
|
68
|
+
* @param {Object|string} [options] - Options or encoding string
|
|
69
|
+
* @param {string} [options.encoding] - Text encoding ('utf8', 'base64', etc.)
|
|
70
|
+
* @returns {Promise<Buffer|string>} File content
|
|
71
|
+
*/
|
|
72
|
+
async readFile(filePath, options = {}) {
|
|
73
|
+
await this.initialize();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Handle options parameter
|
|
77
|
+
if (typeof options === 'string') {
|
|
78
|
+
options = { encoding: options };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.pathConverter.validatePath(filePath);
|
|
82
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
83
|
+
|
|
84
|
+
// Get object stream
|
|
85
|
+
const stream = await this.minioClient.getObject(bucket, key);
|
|
86
|
+
|
|
87
|
+
// Convert stream to buffer
|
|
88
|
+
const buffer = await StreamConverter.streamToBuffer(stream);
|
|
89
|
+
|
|
90
|
+
// Return string if encoding specified, otherwise buffer
|
|
91
|
+
if (options.encoding) {
|
|
92
|
+
return buffer.toString(options.encoding);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return buffer;
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw ErrorHandler.convertError(error, filePath, 'open');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write file content
|
|
104
|
+
* @param {string} filePath - File path
|
|
105
|
+
* @param {string|Buffer|Uint8Array} data - Data to write
|
|
106
|
+
* @param {Object|string} [options] - Options or encoding string
|
|
107
|
+
* @param {string} [options.encoding='utf8'] - Text encoding for string data
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async writeFile(filePath, data, options = {}) {
|
|
111
|
+
await this.initialize();
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Handle options parameter
|
|
115
|
+
if (typeof options === 'string') {
|
|
116
|
+
options = { encoding: options };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.pathConverter.validatePath(filePath);
|
|
120
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
121
|
+
|
|
122
|
+
// Convert data to stream
|
|
123
|
+
const stream = StreamConverter.toReadableStream(data);
|
|
124
|
+
const size = StreamConverter.getDataSize(data);
|
|
125
|
+
|
|
126
|
+
// Upload object
|
|
127
|
+
if (size !== undefined) {
|
|
128
|
+
await this.minioClient.putObject(bucket, key, stream, size);
|
|
129
|
+
} else {
|
|
130
|
+
await this.minioClient.putObject(bucket, key, stream);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw ErrorHandler.convertError(error, filePath, 'open');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if file exists
|
|
140
|
+
* @param {string} filePath - File path
|
|
141
|
+
* @returns {Promise<boolean>} True if file exists
|
|
142
|
+
*/
|
|
143
|
+
async exists(filePath) {
|
|
144
|
+
try {
|
|
145
|
+
await this.stat(filePath);
|
|
146
|
+
return true;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (ErrorHandler.isNotFoundError(error)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get file statistics
|
|
157
|
+
* @param {string} filePath - File path
|
|
158
|
+
* @returns {Promise<Object>} File stats object
|
|
159
|
+
*/
|
|
160
|
+
async stat(filePath) {
|
|
161
|
+
await this.initialize();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
this.pathConverter.validatePath(filePath);
|
|
165
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
166
|
+
|
|
167
|
+
// Get object stats
|
|
168
|
+
const objInfo = await this.minioClient.statObject(bucket, key);
|
|
169
|
+
|
|
170
|
+
// Convert to fs.Stats-like object
|
|
171
|
+
return {
|
|
172
|
+
isFile: () => true,
|
|
173
|
+
isDirectory: () => false,
|
|
174
|
+
isBlockDevice: () => false,
|
|
175
|
+
isCharacterDevice: () => false,
|
|
176
|
+
isSymbolicLink: () => false,
|
|
177
|
+
isFIFO: () => false,
|
|
178
|
+
isSocket: () => false,
|
|
179
|
+
size: objInfo.size,
|
|
180
|
+
mode: 0o644, // Default file permissions
|
|
181
|
+
uid: 0,
|
|
182
|
+
gid: 0,
|
|
183
|
+
atime: objInfo.lastModified,
|
|
184
|
+
mtime: objInfo.lastModified,
|
|
185
|
+
ctime: objInfo.lastModified,
|
|
186
|
+
birthtime: objInfo.lastModified,
|
|
187
|
+
dev: 0,
|
|
188
|
+
ino: 0,
|
|
189
|
+
nlink: 1,
|
|
190
|
+
rdev: 0,
|
|
191
|
+
blocks: Math.ceil(objInfo.size / 512),
|
|
192
|
+
blksize: 4096,
|
|
193
|
+
etag: objInfo.etag
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw ErrorHandler.convertError(error, filePath, 'stat');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Delete file
|
|
203
|
+
* @param {string} filePath - File path
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async unlink(filePath) {
|
|
207
|
+
await this.initialize();
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
this.pathConverter.validatePath(filePath);
|
|
211
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
212
|
+
|
|
213
|
+
// Check if file exists first
|
|
214
|
+
try {
|
|
215
|
+
await this.minioClient.statObject(bucket, key);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// If file doesn't exist, throw ENOENT error
|
|
218
|
+
throw ErrorHandler.convertError(error, filePath, 'unlink');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Remove object
|
|
222
|
+
await this.minioClient.removeObject(bucket, key);
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (!ErrorHandler.isNotFoundError(error)) {
|
|
226
|
+
throw ErrorHandler.convertError(error, filePath, 'unlink');
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Read directory contents
|
|
234
|
+
* @param {string} dirPath - Directory path
|
|
235
|
+
* @param {Object} [options] - Options
|
|
236
|
+
* @param {boolean} [options.withFileTypes=false] - Return Dirent objects
|
|
237
|
+
* @returns {Promise<string[]|Object[]>} Directory contents
|
|
238
|
+
*/
|
|
239
|
+
async readdir(dirPath, options = {}) {
|
|
240
|
+
await this.initialize();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
this.pathConverter.validatePath(dirPath);
|
|
244
|
+
const { bucket, prefix } = this.pathConverter.getListPrefix(dirPath);
|
|
245
|
+
|
|
246
|
+
const objects = [];
|
|
247
|
+
const objectStream = this.minioClient.listObjectsV2(bucket, prefix, false);
|
|
248
|
+
|
|
249
|
+
// Collect all objects
|
|
250
|
+
await new Promise((resolve, reject) => {
|
|
251
|
+
objectStream.on('data', (obj) => {
|
|
252
|
+
objects.push(obj);
|
|
253
|
+
});
|
|
254
|
+
objectStream.on('end', resolve);
|
|
255
|
+
objectStream.on('error', reject);
|
|
256
|
+
}); // Extract filenames and remove prefix
|
|
257
|
+
const filenames = objects
|
|
258
|
+
.map(obj => {
|
|
259
|
+
// Handle both regular objects and prefix objects
|
|
260
|
+
let name = obj.name || obj.prefix;
|
|
261
|
+
if (!name) {
|
|
262
|
+
console.log('Warning: Object missing name/prefix property:', obj);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (prefix && name.startsWith(prefix)) {
|
|
266
|
+
name = name.substring(prefix.length);
|
|
267
|
+
}
|
|
268
|
+
// Remove trailing slash from directory names
|
|
269
|
+
if (name.endsWith('/')) {
|
|
270
|
+
name = name.slice(0, -1);
|
|
271
|
+
}
|
|
272
|
+
return name;
|
|
273
|
+
})
|
|
274
|
+
.filter(name => name !== null && name.length > 0) // Remove null and empty names
|
|
275
|
+
.filter(name => !name.includes('/')) // Only direct children
|
|
276
|
+
.sort();
|
|
277
|
+
|
|
278
|
+
// Return Dirent objects if requested
|
|
279
|
+
if (options.withFileTypes) {
|
|
280
|
+
return filenames.map(name => ({
|
|
281
|
+
name,
|
|
282
|
+
isFile: () => true,
|
|
283
|
+
isDirectory: () => false,
|
|
284
|
+
isBlockDevice: () => false,
|
|
285
|
+
isCharacterDevice: () => false,
|
|
286
|
+
isSymbolicLink: () => false,
|
|
287
|
+
isFIFO: () => false,
|
|
288
|
+
isSocket: () => false
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return filenames;
|
|
293
|
+
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw ErrorHandler.convertError(error, dirPath, 'scandir');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create directory (creates directory marker object)
|
|
301
|
+
* @param {string} dirPath - Directory path
|
|
302
|
+
* @param {Object} [options] - Options
|
|
303
|
+
* @param {boolean} [options.recursive=false] - Create parent directories
|
|
304
|
+
* @returns {Promise<void>}
|
|
305
|
+
*/
|
|
306
|
+
async mkdir(dirPath, options = {}) {
|
|
307
|
+
await this.initialize();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
this.pathConverter.validatePath(dirPath);
|
|
311
|
+
// Create parent directories if recursive option is enabled
|
|
312
|
+
if (options.recursive) {
|
|
313
|
+
const parent = this.pathConverter.getParentPath(dirPath);
|
|
314
|
+
if (parent !== '/' && parent !== dirPath) {
|
|
315
|
+
// Recursively create parent directory without checking if it exists first
|
|
316
|
+
await this.mkdir(parent, { recursive: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const { bucket, key } = this.pathConverter.createDirectoryMarker(dirPath); // Check if directory already exists
|
|
321
|
+
try {
|
|
322
|
+
await this.minioClient.statObject(bucket, key);
|
|
323
|
+
// Directory already exists
|
|
324
|
+
if (!options.recursive) {
|
|
325
|
+
throw ErrorHandler.createError('EEXIST', dirPath, 'mkdir');
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// If error is not "Not Found", re-throw it
|
|
330
|
+
if (error.code !== 'NotFound' &&
|
|
331
|
+
!error.message.includes('Not Found') &&
|
|
332
|
+
!ErrorHandler.isNotFoundError(error)) {
|
|
333
|
+
throw ErrorHandler.convertError(error, dirPath, 'mkdir');
|
|
334
|
+
}
|
|
335
|
+
// Otherwise, directory doesn't exist, continue to create it
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create empty directory marker object
|
|
339
|
+
const emptyStream = StreamConverter.toReadableStream('');
|
|
340
|
+
await this.minioClient.putObject(bucket, key, emptyStream, 0);
|
|
341
|
+
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (!ErrorHandler.isExistsError(error)) {
|
|
344
|
+
throw ErrorHandler.convertError(error, dirPath, 'mkdir');
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Remove directory
|
|
352
|
+
* @param {string} dirPath - Directory path
|
|
353
|
+
* @returns {Promise<void>}
|
|
354
|
+
*/
|
|
355
|
+
async rmdir(dirPath) {
|
|
356
|
+
await this.initialize();
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
this.pathConverter.validatePath(dirPath);
|
|
360
|
+
|
|
361
|
+
// Check if directory is empty
|
|
362
|
+
const contents = await this.readdir(dirPath);
|
|
363
|
+
if (contents.length > 0) {
|
|
364
|
+
throw ErrorHandler.createError('ENOTEMPTY', dirPath, 'rmdir');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Remove directory marker
|
|
368
|
+
const { bucket, key } = this.pathConverter.createDirectoryMarker(dirPath);
|
|
369
|
+
await this.minioClient.removeObject(bucket, key);
|
|
370
|
+
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (!ErrorHandler.isNotFoundError(error) && error.code !== 'ENOTEMPTY') {
|
|
373
|
+
throw ErrorHandler.convertError(error, dirPath, 'rmdir');
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Create read stream
|
|
381
|
+
* @param {string} filePath - File path
|
|
382
|
+
* @param {Object} [options] - Stream options
|
|
383
|
+
* @returns {Promise<Readable>} Read stream
|
|
384
|
+
*/
|
|
385
|
+
async createReadStream(filePath, options = {}) {
|
|
386
|
+
await this.initialize();
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
this.pathConverter.validatePath(filePath);
|
|
390
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
391
|
+
|
|
392
|
+
// Get object stream
|
|
393
|
+
const stream = await this.minioClient.getObject(bucket, key);
|
|
394
|
+
return stream;
|
|
395
|
+
|
|
396
|
+
} catch (error) {
|
|
397
|
+
throw ErrorHandler.convertError(error, filePath, 'open');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create write stream
|
|
403
|
+
* @param {string} filePath - File path
|
|
404
|
+
* @param {Object} [options] - Stream options
|
|
405
|
+
* @returns {Writable} Write stream
|
|
406
|
+
*/
|
|
407
|
+
createWriteStream(filePath, options = {}) {
|
|
408
|
+
const passThrough = StreamConverter.createPassThrough();
|
|
409
|
+
|
|
410
|
+
// Handle the upload asynchronously
|
|
411
|
+
this.initialize()
|
|
412
|
+
.then(() => {
|
|
413
|
+
this.pathConverter.validatePath(filePath);
|
|
414
|
+
const { bucket, key } = this.pathConverter.pathToMinIO(filePath);
|
|
415
|
+
|
|
416
|
+
return this.minioClient.putObject(bucket, key, passThrough);
|
|
417
|
+
})
|
|
418
|
+
.catch(error => {
|
|
419
|
+
passThrough.destroy(ErrorHandler.convertError(error, filePath, 'open'));
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return passThrough;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Copy file
|
|
427
|
+
* @param {string} srcPath - Source file path
|
|
428
|
+
* @param {string} destPath - Destination file path
|
|
429
|
+
* @returns {Promise<void>}
|
|
430
|
+
*/
|
|
431
|
+
async copyFile(srcPath, destPath) {
|
|
432
|
+
await this.initialize();
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
this.pathConverter.validatePath(srcPath);
|
|
436
|
+
this.pathConverter.validatePath(destPath);
|
|
437
|
+
|
|
438
|
+
const srcMinIO = this.pathConverter.pathToMinIO(srcPath);
|
|
439
|
+
const destMinIO = this.pathConverter.pathToMinIO(destPath);
|
|
440
|
+
|
|
441
|
+
// Copy object within MinIO
|
|
442
|
+
const copyConditions = new this.minioClient.CopyConditions();
|
|
443
|
+
await this.minioClient.copyObject(
|
|
444
|
+
destMinIO.bucket,
|
|
445
|
+
destMinIO.key,
|
|
446
|
+
`/${srcMinIO.bucket}/${srcMinIO.key}`,
|
|
447
|
+
copyConditions
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw ErrorHandler.convertError(error, srcPath, 'open');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get MinIO client instance for advanced operations
|
|
457
|
+
* @returns {MinioClient} MinIO client
|
|
458
|
+
*/
|
|
459
|
+
getMinioClient() {
|
|
460
|
+
return this.minioClient;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get bucket name
|
|
465
|
+
* @returns {string} Bucket name
|
|
466
|
+
*/
|
|
467
|
+
getBucket() {
|
|
468
|
+
return this.bucket;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Get path converter instance
|
|
473
|
+
* @returns {PathConverter} Path converter
|
|
474
|
+
*/
|
|
475
|
+
getPathConverter() {
|
|
476
|
+
return this.pathConverter;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export default FsMinioClient;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// PathConverter.js - Filesystem path to MinIO bucket/key conversion
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
class PathConverter {
|
|
6
|
+
/**
|
|
7
|
+
* Create PathConverter instance
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.bucket - Default bucket name
|
|
10
|
+
* @param {string} [options.prefix=''] - Key prefix for all objects
|
|
11
|
+
* @param {string} [options.separator='/'] - Path separator for object keys
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.bucket = options.bucket;
|
|
15
|
+
this.prefix = options.prefix || '';
|
|
16
|
+
this.separator = options.separator || '/';
|
|
17
|
+
|
|
18
|
+
if (!this.bucket) {
|
|
19
|
+
throw new Error('Bucket name is required');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert filesystem path to MinIO bucket and key
|
|
25
|
+
* @param {string} filePath - Filesystem path (e.g., '/data/file.txt')
|
|
26
|
+
* @returns {Object} {bucket: string, key: string}
|
|
27
|
+
*/
|
|
28
|
+
pathToMinIO(filePath) {
|
|
29
|
+
// Normalize path and remove leading slash
|
|
30
|
+
const normalizedPath = path.posix.normalize(filePath).replace(/^\/+/, '');
|
|
31
|
+
|
|
32
|
+
// Combine prefix and path
|
|
33
|
+
let key = normalizedPath;
|
|
34
|
+
if (this.prefix) {
|
|
35
|
+
key = this.prefix + this.separator + normalizedPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure we don't have double separators
|
|
39
|
+
key = key.replace(/\/+/g, '/');
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
bucket: this.bucket,
|
|
43
|
+
key: key
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert MinIO bucket and key back to filesystem path
|
|
49
|
+
* @param {string} bucket - Bucket name
|
|
50
|
+
* @param {string} key - Object key
|
|
51
|
+
* @returns {string} Filesystem path
|
|
52
|
+
*/
|
|
53
|
+
minIOToPath(bucket, key) {
|
|
54
|
+
let filePath = key;
|
|
55
|
+
|
|
56
|
+
// Remove prefix if it exists
|
|
57
|
+
if (this.prefix && key.startsWith(this.prefix + this.separator)) {
|
|
58
|
+
filePath = key.substring(this.prefix.length + this.separator.length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure leading slash for absolute path
|
|
62
|
+
if (!filePath.startsWith('/')) {
|
|
63
|
+
filePath = '/' + filePath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return path.posix.normalize(filePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get parent directory path
|
|
71
|
+
* @param {string} filePath - File path
|
|
72
|
+
* @returns {string} Parent directory path
|
|
73
|
+
*/
|
|
74
|
+
getParentPath(filePath) {
|
|
75
|
+
const normalized = path.posix.normalize(filePath);
|
|
76
|
+
const parent = path.posix.dirname(normalized);
|
|
77
|
+
return parent === '.' ? '/' : parent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if path represents a directory (ends with separator)
|
|
82
|
+
* @param {string} filePath - Path to check
|
|
83
|
+
* @returns {boolean} True if directory path
|
|
84
|
+
*/
|
|
85
|
+
isDirectoryPath(filePath) {
|
|
86
|
+
return filePath.endsWith(this.separator);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert path to directory path (ensure trailing separator)
|
|
91
|
+
* @param {string} filePath - Path to convert
|
|
92
|
+
* @returns {string} Directory path with trailing separator
|
|
93
|
+
*/
|
|
94
|
+
toDirectoryPath(filePath) {
|
|
95
|
+
const normalized = path.posix.normalize(filePath);
|
|
96
|
+
return normalized.endsWith(this.separator) ? normalized : normalized + this.separator;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get MinIO key for directory listing
|
|
101
|
+
* @param {string} dirPath - Directory path
|
|
102
|
+
* @returns {Object} {bucket: string, prefix: string}
|
|
103
|
+
*/
|
|
104
|
+
getListPrefix(dirPath) {
|
|
105
|
+
const { bucket, key } = this.pathToMinIO(dirPath);
|
|
106
|
+
|
|
107
|
+
// For directory listing, we need a prefix that ends with separator
|
|
108
|
+
let prefix = key;
|
|
109
|
+
if (prefix && !prefix.endsWith(this.separator)) {
|
|
110
|
+
prefix += this.separator;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
bucket,
|
|
115
|
+
prefix
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create directory marker key (empty object representing directory)
|
|
121
|
+
* @param {string} dirPath - Directory path
|
|
122
|
+
* @returns {Object} {bucket: string, key: string}
|
|
123
|
+
*/
|
|
124
|
+
createDirectoryMarker(dirPath) {
|
|
125
|
+
const dirPathWithSeparator = this.toDirectoryPath(dirPath);
|
|
126
|
+
return this.pathToMinIO(dirPathWithSeparator);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract filename from path
|
|
131
|
+
* @param {string} filePath - Full file path
|
|
132
|
+
* @returns {string} Filename
|
|
133
|
+
*/
|
|
134
|
+
getFileName(filePath) {
|
|
135
|
+
return path.posix.basename(filePath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Join path segments
|
|
140
|
+
* @param {...string} segments - Path segments to join
|
|
141
|
+
* @returns {string} Joined path
|
|
142
|
+
*/
|
|
143
|
+
joinPath(...segments) {
|
|
144
|
+
return path.posix.join(...segments);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if path is absolute
|
|
149
|
+
* @param {string} filePath - Path to check
|
|
150
|
+
* @returns {boolean} True if absolute path
|
|
151
|
+
*/
|
|
152
|
+
isAbsolute(filePath) {
|
|
153
|
+
return path.posix.isAbsolute(filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve relative path to absolute
|
|
158
|
+
* @param {string} basePath - Base path
|
|
159
|
+
* @param {string} relativePath - Relative path
|
|
160
|
+
* @returns {string} Absolute path
|
|
161
|
+
*/
|
|
162
|
+
resolve(basePath, relativePath) {
|
|
163
|
+
if (this.isAbsolute(relativePath)) {
|
|
164
|
+
return relativePath;
|
|
165
|
+
}
|
|
166
|
+
return this.joinPath(basePath, relativePath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate path format
|
|
171
|
+
* @param {string} filePath - Path to validate
|
|
172
|
+
* @throws {Error} If path is invalid
|
|
173
|
+
*/
|
|
174
|
+
validatePath(filePath) {
|
|
175
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
176
|
+
throw new Error('Path must be a non-empty string');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for invalid characters
|
|
180
|
+
const invalidChars = /[<>:"|?*\x00-\x1f]/;
|
|
181
|
+
if (invalidChars.test(filePath)) {
|
|
182
|
+
throw new Error('Path contains invalid characters');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check length limits (S3 has 1024 byte limit for keys)
|
|
186
|
+
const { key } = this.pathToMinIO(filePath);
|
|
187
|
+
if (Buffer.byteLength(key, 'utf8') > 1024) {
|
|
188
|
+
throw new Error('Path too long (exceeds 1024 bytes when converted to key)');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get bucket name
|
|
194
|
+
* @returns {string} Bucket name
|
|
195
|
+
*/
|
|
196
|
+
getBucket() {
|
|
197
|
+
return this.bucket;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get prefix
|
|
202
|
+
* @returns {string} Prefix
|
|
203
|
+
*/
|
|
204
|
+
getPrefix() {
|
|
205
|
+
return this.prefix;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default PathConverter;
|