@kadi.build/file-manager 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/README.md +268 -0
- package/package.json +48 -0
- package/src/ConfigManager.js +301 -0
- package/src/FileManager.js +526 -0
- package/src/index.js +48 -0
- package/src/providers/CompressionProvider.js +968 -0
- package/src/providers/LocalProvider.js +824 -0
- package/src/providers/RemoteProvider.js +514 -0
- package/src/providers/WatchProvider.js +611 -0
- package/src/utils/FileStreamingUtils.js +757 -0
- package/src/utils/PathUtils.js +144 -0
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# @kadi.build/file-manager
|
|
2
|
+
|
|
3
|
+
> Foundational file management package providing local and remote file operations, file watching, compression utilities, and file streaming.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Local File Operations** — Upload, download, copy, move, rename, delete files and folders
|
|
8
|
+
- **Remote File Operations** — SSH/SFTP-based remote operations with bastion/jump host support
|
|
9
|
+
- **File Watching** — Real-time file system monitoring with configurable debouncing and filtering
|
|
10
|
+
- **Compression** — ZIP and TAR.GZ compression/decompression with progress tracking and batch operations
|
|
11
|
+
- **File Streaming** — Memory-efficient streaming with range request support, ETag generation, and download tracking
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @kadi.build/file-manager
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { createFileManager } from '@kadi.build/file-manager';
|
|
23
|
+
|
|
24
|
+
// Create a file manager instance
|
|
25
|
+
const fm = await createFileManager();
|
|
26
|
+
|
|
27
|
+
// Local file operations
|
|
28
|
+
await fm.uploadFile('./source.txt', './destination.txt');
|
|
29
|
+
const info = await fm.getFileInfo('./myfile.txt');
|
|
30
|
+
const files = await fm.listFiles('./my-directory');
|
|
31
|
+
|
|
32
|
+
// Compression
|
|
33
|
+
await fm.compressFile('./folder', './backup.zip', { format: 'zip' });
|
|
34
|
+
await fm.decompressFile('./backup.zip', './extracted/');
|
|
35
|
+
|
|
36
|
+
// File watching
|
|
37
|
+
const watcher = await fm.startWatching('./watched-dir');
|
|
38
|
+
fm.on('fileEvent', (event) => {
|
|
39
|
+
console.log(`${event.type}: ${event.path}`);
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API Reference
|
|
44
|
+
|
|
45
|
+
### FileManager
|
|
46
|
+
|
|
47
|
+
The main orchestrator that provides a unified interface to all providers.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
import { FileManager, ConfigManager } from '@kadi.build/file-manager';
|
|
51
|
+
|
|
52
|
+
const config = new ConfigManager();
|
|
53
|
+
await config.load();
|
|
54
|
+
const fm = new FileManager(config);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Connection & Validation
|
|
58
|
+
|
|
59
|
+
| Method | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `testConnection(providerName)` | Test connectivity for a specific provider (`'local'`, `'watch'`, `'compression'`, `'remote'`) |
|
|
62
|
+
| `validateProvider(providerName)` | Validate provider configuration |
|
|
63
|
+
| `getSystemInfo()` | Get system information (platform, memory, disk space) |
|
|
64
|
+
|
|
65
|
+
#### File Operations
|
|
66
|
+
|
|
67
|
+
| Method | Description |
|
|
68
|
+
|--------|-------------|
|
|
69
|
+
| `uploadFile(source, dest, providerName?)` | Copy/upload a file |
|
|
70
|
+
| `downloadFile(source, dest, providerName?)` | Copy/download a file |
|
|
71
|
+
| `getFileInfo(filePath, providerName?)` | Get file metadata |
|
|
72
|
+
| `listFiles(directory, providerName?, options?)` | List files in a directory |
|
|
73
|
+
| `deleteFile(filePath, providerName?)` | Delete a file |
|
|
74
|
+
| `renameFile(oldPath, newPath, providerName?)` | Rename a file |
|
|
75
|
+
| `copyFile(source, dest, providerName?)` | Copy a file |
|
|
76
|
+
| `moveFile(source, dest, providerName?)` | Move a file |
|
|
77
|
+
| `searchFiles(directory, pattern, options?)` | Search for files matching a pattern |
|
|
78
|
+
|
|
79
|
+
#### Folder Operations
|
|
80
|
+
|
|
81
|
+
| Method | Description |
|
|
82
|
+
|--------|-------------|
|
|
83
|
+
| `createFolder(folderPath, providerName?)` | Create a directory (recursive) |
|
|
84
|
+
| `listFolders(directory, providerName?)` | List subdirectories |
|
|
85
|
+
| `deleteFolder(folderPath, options?)` | Delete a directory |
|
|
86
|
+
| `renameFolder(oldPath, newPath)` | Rename a directory |
|
|
87
|
+
| `copyFolder(source, dest)` | Copy a directory recursively |
|
|
88
|
+
| `moveFolder(source, dest)` | Move a directory |
|
|
89
|
+
| `getFolderInfo(folderPath)` | Get directory metadata |
|
|
90
|
+
|
|
91
|
+
#### Compression
|
|
92
|
+
|
|
93
|
+
| Method | Description |
|
|
94
|
+
|--------|-------------|
|
|
95
|
+
| `compressFile(input, output, options?)` | Compress a file or directory |
|
|
96
|
+
| `decompressFile(archive, outputDir, options?)` | Decompress an archive |
|
|
97
|
+
| `compress(input, output, options?)` | Alias for `compressFile` |
|
|
98
|
+
| `decompress(archive, outputDir, options?)` | Alias for `decompressFile` |
|
|
99
|
+
| `compressMultipleFiles(files, outputDir, options?)` | Batch compress multiple files |
|
|
100
|
+
| `decompressMultipleFiles(archives, outputDir, options?)` | Batch decompress multiple archives |
|
|
101
|
+
| `getCompressionStatus()` | Get compression provider status |
|
|
102
|
+
| `getCompressionProvider()` | Get the underlying compression provider |
|
|
103
|
+
|
|
104
|
+
**Compression Options:**
|
|
105
|
+
```javascript
|
|
106
|
+
{
|
|
107
|
+
format: 'zip' | 'tar.gz', // Archive format (default: 'zip')
|
|
108
|
+
level: 1-9, // Compression level (default: 6)
|
|
109
|
+
includeRoot: true, // Include root directory in archive
|
|
110
|
+
overwrite: true // Overwrite existing files during extraction
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### File Watching
|
|
115
|
+
|
|
116
|
+
| Method | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `startWatching(path, options?)` | Start watching a path for changes |
|
|
119
|
+
| `stopWatching(watchIdOrPath)` | Stop a specific watcher |
|
|
120
|
+
| `stopAllWatching()` | Stop all active watchers |
|
|
121
|
+
| `listActiveWatchers()` | List all active watchers |
|
|
122
|
+
| `getWatcherInfo(watchId)` | Get details about a watcher |
|
|
123
|
+
| `getWatchingStatus()` | Get overall watching status |
|
|
124
|
+
|
|
125
|
+
**Watch Options:**
|
|
126
|
+
```javascript
|
|
127
|
+
{
|
|
128
|
+
recursive: true, // Watch subdirectories
|
|
129
|
+
events: ['add', 'change', 'unlink'], // Event types to listen for
|
|
130
|
+
ignoreDotfiles: false, // Ignore dotfiles
|
|
131
|
+
debounceMs: 100 // Debounce interval in ms
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Events:**
|
|
136
|
+
```javascript
|
|
137
|
+
fm.on('fileEvent', (event) => {
|
|
138
|
+
// event.type: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
|
139
|
+
// event.path: string
|
|
140
|
+
// event.watchId: string
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Remote Operations
|
|
145
|
+
|
|
146
|
+
| Method | Description |
|
|
147
|
+
|--------|-------------|
|
|
148
|
+
| `connectRemote()` | Connect to the remote server |
|
|
149
|
+
| `disconnectRemote()` | Disconnect from the remote server |
|
|
150
|
+
| `syncToRemote(localPath, remotePath)` | Upload files to remote |
|
|
151
|
+
| `syncFromRemote(remotePath, localPath)` | Download files from remote |
|
|
152
|
+
|
|
153
|
+
**Remote Configuration (via environment variables):**
|
|
154
|
+
```
|
|
155
|
+
KADI_REMOTE_HOST=example.com
|
|
156
|
+
KADI_REMOTE_PORT=22
|
|
157
|
+
KADI_REMOTE_USERNAME=user
|
|
158
|
+
KADI_REMOTE_PASSWORD=pass
|
|
159
|
+
KADI_REMOTE_PRIVATE_KEY_PATH=~/.ssh/id_rsa
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Individual Providers
|
|
163
|
+
|
|
164
|
+
For advanced use cases, you can import providers directly:
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
import {
|
|
168
|
+
LocalProvider,
|
|
169
|
+
WatchProvider,
|
|
170
|
+
CompressionProvider,
|
|
171
|
+
RemoteProvider,
|
|
172
|
+
FileStreamingUtils,
|
|
173
|
+
FileStreamer,
|
|
174
|
+
DownloadTracker,
|
|
175
|
+
PathUtils
|
|
176
|
+
} from '@kadi.build/file-manager';
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Factory Functions
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
import { createFileManager, compressFile, decompressFile, watchDirectory } from '@kadi.build/file-manager';
|
|
183
|
+
|
|
184
|
+
// Quick file manager creation
|
|
185
|
+
const fm = await createFileManager(options);
|
|
186
|
+
|
|
187
|
+
// One-shot compression
|
|
188
|
+
await compressFile('./input', './output.zip', { format: 'zip' });
|
|
189
|
+
|
|
190
|
+
// One-shot decompression
|
|
191
|
+
await decompressFile('./archive.zip', './output/');
|
|
192
|
+
|
|
193
|
+
// Quick directory watching
|
|
194
|
+
const watcher = await watchDirectory('./path', (event) => {
|
|
195
|
+
console.log(event);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
Configuration is loaded from multiple sources (in priority order):
|
|
202
|
+
|
|
203
|
+
1. Constructor options
|
|
204
|
+
2. Environment variables (prefixed with `KADI_`)
|
|
205
|
+
3. `.env` file
|
|
206
|
+
|
|
207
|
+
| Variable | Description | Default |
|
|
208
|
+
|----------|-------------|---------|
|
|
209
|
+
| `KADI_LOCAL_ROOT` | Root directory for local operations | `.` |
|
|
210
|
+
| `KADI_WATCH_DEBOUNCE_MS` | Debounce interval for file watching | `100` |
|
|
211
|
+
| `KADI_WATCH_MAX_WATCHERS` | Maximum concurrent watchers | `10` |
|
|
212
|
+
| `KADI_COMPRESSION_FORMAT` | Default compression format | `zip` |
|
|
213
|
+
| `KADI_COMPRESSION_LEVEL` | Default compression level | `6` |
|
|
214
|
+
| `KADI_REMOTE_HOST` | Remote SSH host | — |
|
|
215
|
+
| `KADI_REMOTE_PORT` | Remote SSH port | `22` |
|
|
216
|
+
| `KADI_REMOTE_USERNAME` | Remote SSH username | — |
|
|
217
|
+
| `KADI_REMOTE_PASSWORD` | Remote SSH password | — |
|
|
218
|
+
| `KADI_REMOTE_PRIVATE_KEY_PATH` | Path to SSH private key | — |
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Run all tests
|
|
224
|
+
npm test
|
|
225
|
+
|
|
226
|
+
# Run individual test suites
|
|
227
|
+
npm run test:local # Local file operations (33 tests)
|
|
228
|
+
npm run test:watch # File watching (30 tests)
|
|
229
|
+
npm run test:compression # Compression (42 tests)
|
|
230
|
+
npm run test:streaming # File streaming (12 tests)
|
|
231
|
+
npm run test:remote # Remote operations (18 tests)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Total: 135 tests across 5 suites**
|
|
235
|
+
|
|
236
|
+
## Project Structure
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
packages/file-manager/
|
|
240
|
+
├── package.json
|
|
241
|
+
├── README.md
|
|
242
|
+
├── src/
|
|
243
|
+
│ ├── index.js # Main entry point & exports
|
|
244
|
+
│ ├── FileManager.js # Main orchestrator
|
|
245
|
+
│ ├── ConfigManager.js # Configuration management
|
|
246
|
+
│ ├── providers/
|
|
247
|
+
│ │ ├── LocalProvider.js # Local file operations
|
|
248
|
+
│ │ ├── WatchProvider.js # File watching (chokidar)
|
|
249
|
+
│ │ ├── CompressionProvider.js # ZIP & TAR.GZ compression
|
|
250
|
+
│ │ └── RemoteProvider.js # SSH/SFTP remote operations
|
|
251
|
+
│ └── utils/
|
|
252
|
+
│ ├── FileStreamingUtils.js # Streaming, MIME detection, ETags
|
|
253
|
+
│ └── PathUtils.js # Path validation & normalization
|
|
254
|
+
└── tests/
|
|
255
|
+
├── run-all-tests.js
|
|
256
|
+
├── test-utils.js
|
|
257
|
+
├── test-helpers/
|
|
258
|
+
│ └── index.js
|
|
259
|
+
├── test-local-operations.js
|
|
260
|
+
├── test-file-watching.js
|
|
261
|
+
├── test-compression.js
|
|
262
|
+
├── test-file-streaming.js
|
|
263
|
+
└── test-remote-operations.js
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kadi.build/file-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Complete local and remote file management with watching and compression",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node tests/run-all-tests.js",
|
|
18
|
+
"test:local": "node tests/test-local-operations.js",
|
|
19
|
+
"test:watch": "node tests/test-file-watching.js",
|
|
20
|
+
"test:compression": "node tests/test-compression.js",
|
|
21
|
+
"test:streaming": "node tests/test-file-streaming.js",
|
|
22
|
+
"test:remote": "node tests/test-remote-operations.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"file-management",
|
|
26
|
+
"file-operations",
|
|
27
|
+
"file-watching",
|
|
28
|
+
"compression",
|
|
29
|
+
"sftp",
|
|
30
|
+
"ssh",
|
|
31
|
+
"remote-files"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chokidar": "^3.5.3",
|
|
35
|
+
"archiver": "^6.0.0",
|
|
36
|
+
"unzipper": "^0.10.14",
|
|
37
|
+
"tar": "^6.2.0",
|
|
38
|
+
"ssh2": "^1.15.0",
|
|
39
|
+
"ssh2-sftp-client": "^9.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"fs-extra": "^11.2.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigManager for @kadi.build/file-manager
|
|
3
|
+
*
|
|
4
|
+
* Handles configuration for file operations only.
|
|
5
|
+
* Simplified from the monolith ConfigManager - no tunnel/server config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
|
|
12
|
+
class ConfigManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.config = {};
|
|
15
|
+
this.defaults = {
|
|
16
|
+
// Local File System Configuration
|
|
17
|
+
LOCAL_ROOT: process.cwd(),
|
|
18
|
+
DEFAULT_UPLOAD_DIRECTORY: './uploads',
|
|
19
|
+
DEFAULT_DOWNLOAD_DIRECTORY: './downloads',
|
|
20
|
+
DEFAULT_TEMP_DIRECTORY: './temp',
|
|
21
|
+
|
|
22
|
+
// File Operation Configuration
|
|
23
|
+
MAX_FILE_SIZE: 1073741824, // 1GB in bytes
|
|
24
|
+
CHUNK_SIZE: 8388608, // 8MB chunks
|
|
25
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
26
|
+
TIMEOUT_MS: 30000,
|
|
27
|
+
|
|
28
|
+
// File Watching Configuration
|
|
29
|
+
WATCH_ENABLED: true,
|
|
30
|
+
WATCH_RECURSIVE: true,
|
|
31
|
+
WATCH_IGNORE_DOTFILES: true,
|
|
32
|
+
WATCH_DEBOUNCE_MS: 100,
|
|
33
|
+
WATCH_MAX_WATCHERS: 50,
|
|
34
|
+
|
|
35
|
+
// Compression Configuration
|
|
36
|
+
COMPRESSION_ENABLED: true,
|
|
37
|
+
COMPRESSION_LEVEL: 6,
|
|
38
|
+
COMPRESSION_FORMAT: 'zip',
|
|
39
|
+
COMPRESSION_MAX_FILE_SIZE: 1073741824,
|
|
40
|
+
COMPRESSION_CHUNK_SIZE: 8388608,
|
|
41
|
+
COMPRESSION_ENABLE_PROGRESS_TRACKING: true,
|
|
42
|
+
COMPRESSION_ENABLE_CHECKSUM_VERIFICATION: true,
|
|
43
|
+
|
|
44
|
+
// Remote Configuration
|
|
45
|
+
REMOTE_HOST: '',
|
|
46
|
+
REMOTE_PORT: 22,
|
|
47
|
+
REMOTE_USERNAME: '',
|
|
48
|
+
REMOTE_PASSWORD: '',
|
|
49
|
+
REMOTE_PRIVATE_KEY_PATH: '~/.ssh/id_rsa',
|
|
50
|
+
REMOTE_PASSPHRASE: '',
|
|
51
|
+
REMOTE_ROOT: '/home',
|
|
52
|
+
REMOTE_TIMEOUT: 30000,
|
|
53
|
+
REMOTE_BASTION_HOST: '',
|
|
54
|
+
REMOTE_BASTION_PORT: 22,
|
|
55
|
+
|
|
56
|
+
// Performance Configuration
|
|
57
|
+
MAX_CONCURRENT_OPERATIONS: 5,
|
|
58
|
+
ENABLE_PROGRESS_TRACKING: true,
|
|
59
|
+
ENABLE_CHECKSUM_VERIFICATION: true,
|
|
60
|
+
|
|
61
|
+
// Security Configuration
|
|
62
|
+
ALLOW_SYMLINKS: false,
|
|
63
|
+
RESTRICT_TO_BASE_PATH: true,
|
|
64
|
+
MAX_PATH_LENGTH: 255,
|
|
65
|
+
|
|
66
|
+
// Logging
|
|
67
|
+
LOG_LEVEL: 'info'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async load(options = {}) {
|
|
72
|
+
// Apply defaults
|
|
73
|
+
this.config = { ...this.defaults };
|
|
74
|
+
|
|
75
|
+
// Load from .env file if it exists
|
|
76
|
+
try {
|
|
77
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
78
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
79
|
+
this._parseEnvContent(envContent);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// .env file not found, that's fine
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load from environment variables
|
|
85
|
+
this._loadFromEnvironment();
|
|
86
|
+
|
|
87
|
+
// Apply passed options (highest priority)
|
|
88
|
+
if (options) {
|
|
89
|
+
for (const [key, value] of Object.entries(options)) {
|
|
90
|
+
// Support both UPPER_CASE config keys and camelCase options
|
|
91
|
+
const upperKey = key.replace(/([A-Z])/g, '_$1').toUpperCase().replace(/^_/, '');
|
|
92
|
+
if (this.defaults.hasOwnProperty(upperKey)) {
|
|
93
|
+
this.config[upperKey] = value;
|
|
94
|
+
} else if (this.defaults.hasOwnProperty(key)) {
|
|
95
|
+
this.config[key] = value;
|
|
96
|
+
} else {
|
|
97
|
+
// Store as-is for custom options
|
|
98
|
+
this.config[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Ensure required directories exist
|
|
104
|
+
await this._ensureDirectories();
|
|
105
|
+
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_parseEnvContent(content) {
|
|
110
|
+
const lines = content.split('\n');
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
114
|
+
|
|
115
|
+
const eqIndex = trimmed.indexOf('=');
|
|
116
|
+
if (eqIndex === -1) continue;
|
|
117
|
+
|
|
118
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
119
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
120
|
+
|
|
121
|
+
// Remove surrounding quotes
|
|
122
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
123
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
124
|
+
value = value.slice(1, -1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Only apply if it's a known config key
|
|
128
|
+
if (this.defaults.hasOwnProperty(key)) {
|
|
129
|
+
this.config[key] = this._parseValue(value, this.defaults[key]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_loadFromEnvironment() {
|
|
135
|
+
for (const key of Object.keys(this.defaults)) {
|
|
136
|
+
// Check KADI_ prefixed env vars
|
|
137
|
+
const envKey = `KADI_${key}`;
|
|
138
|
+
if (process.env[envKey] !== undefined) {
|
|
139
|
+
this.config[key] = this._parseValue(process.env[envKey], this.defaults[key]);
|
|
140
|
+
}
|
|
141
|
+
// Also check non-prefixed env vars
|
|
142
|
+
if (process.env[key] !== undefined) {
|
|
143
|
+
this.config[key] = this._parseValue(process.env[key], this.defaults[key]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_parseValue(value, defaultValue) {
|
|
149
|
+
if (typeof defaultValue === 'boolean') {
|
|
150
|
+
return value === 'true' || value === '1' || value === true;
|
|
151
|
+
}
|
|
152
|
+
if (typeof defaultValue === 'number') {
|
|
153
|
+
const parsed = parseInt(value, 10);
|
|
154
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _ensureDirectories() {
|
|
160
|
+
const dirs = [
|
|
161
|
+
this.config.DEFAULT_UPLOAD_DIRECTORY,
|
|
162
|
+
this.config.DEFAULT_DOWNLOAD_DIRECTORY,
|
|
163
|
+
this.config.DEFAULT_TEMP_DIRECTORY
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const dir of dirs) {
|
|
167
|
+
try {
|
|
168
|
+
await fs.mkdir(dir, { recursive: true });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// Directory creation failures are non-fatal
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// CONFIG GETTERS
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
getLocalConfig() {
|
|
180
|
+
return {
|
|
181
|
+
localRoot: this.config.LOCAL_ROOT,
|
|
182
|
+
uploadDirectory: this.config.DEFAULT_UPLOAD_DIRECTORY,
|
|
183
|
+
downloadDirectory: this.config.DEFAULT_DOWNLOAD_DIRECTORY,
|
|
184
|
+
tempDirectory: this.config.DEFAULT_TEMP_DIRECTORY,
|
|
185
|
+
maxFileSize: this.config.MAX_FILE_SIZE,
|
|
186
|
+
chunkSize: this.config.CHUNK_SIZE,
|
|
187
|
+
maxRetryAttempts: this.config.MAX_RETRY_ATTEMPTS,
|
|
188
|
+
timeoutMs: this.config.TIMEOUT_MS,
|
|
189
|
+
allowSymlinks: this.config.ALLOW_SYMLINKS,
|
|
190
|
+
restrictToBasePath: this.config.RESTRICT_TO_BASE_PATH,
|
|
191
|
+
maxPathLength: this.config.MAX_PATH_LENGTH
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getWatchConfig() {
|
|
196
|
+
return {
|
|
197
|
+
localRoot: this.config.LOCAL_ROOT,
|
|
198
|
+
enabled: this.config.WATCH_ENABLED,
|
|
199
|
+
recursive: this.config.WATCH_RECURSIVE,
|
|
200
|
+
ignoreDotfiles: this.config.WATCH_IGNORE_DOTFILES,
|
|
201
|
+
debounceMs: this.config.WATCH_DEBOUNCE_MS,
|
|
202
|
+
maxWatchers: this.config.WATCH_MAX_WATCHERS
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getCompressionConfig() {
|
|
207
|
+
return {
|
|
208
|
+
localRoot: this.config.LOCAL_ROOT,
|
|
209
|
+
enabled: this.config.COMPRESSION_ENABLED,
|
|
210
|
+
level: this.config.COMPRESSION_LEVEL,
|
|
211
|
+
format: this.config.COMPRESSION_FORMAT,
|
|
212
|
+
maxFileSize: this.config.COMPRESSION_MAX_FILE_SIZE,
|
|
213
|
+
chunkSize: this.config.COMPRESSION_CHUNK_SIZE,
|
|
214
|
+
enableProgressTracking: this.config.COMPRESSION_ENABLE_PROGRESS_TRACKING,
|
|
215
|
+
enableChecksumVerification: this.config.COMPRESSION_ENABLE_CHECKSUM_VERIFICATION
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getRemoteConfig() {
|
|
220
|
+
return {
|
|
221
|
+
host: this.config.REMOTE_HOST,
|
|
222
|
+
port: this.config.REMOTE_PORT,
|
|
223
|
+
username: this.config.REMOTE_USERNAME,
|
|
224
|
+
password: this.config.REMOTE_PASSWORD,
|
|
225
|
+
privateKeyPath: this.config.REMOTE_PRIVATE_KEY_PATH,
|
|
226
|
+
passphrase: this.config.REMOTE_PASSPHRASE,
|
|
227
|
+
remoteRoot: this.config.REMOTE_ROOT,
|
|
228
|
+
timeout: this.config.REMOTE_TIMEOUT,
|
|
229
|
+
bastionHost: this.config.REMOTE_BASTION_HOST,
|
|
230
|
+
bastionPort: this.config.REMOTE_BASTION_PORT
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getPerformanceConfig() {
|
|
235
|
+
return {
|
|
236
|
+
maxConcurrentOperations: this.config.MAX_CONCURRENT_OPERATIONS,
|
|
237
|
+
enableProgressTracking: this.config.ENABLE_PROGRESS_TRACKING,
|
|
238
|
+
enableChecksumVerification: this.config.ENABLE_CHECKSUM_VERIFICATION
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
hasRemoteConfig() {
|
|
243
|
+
return !!(this.config.REMOTE_HOST && this.config.REMOTE_USERNAME);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
validate() {
|
|
247
|
+
const errors = [];
|
|
248
|
+
const warnings = [];
|
|
249
|
+
|
|
250
|
+
if (this.config.MAX_FILE_SIZE <= 0) {
|
|
251
|
+
errors.push('MAX_FILE_SIZE must be positive');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.config.CHUNK_SIZE <= 0) {
|
|
255
|
+
errors.push('CHUNK_SIZE must be positive');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this.config.WATCH_MAX_WATCHERS > 100) {
|
|
259
|
+
warnings.push('High number of watchers may impact performance');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.config.COMPRESSION_LEVEL < 0 || this.config.COMPRESSION_LEVEL > 9) {
|
|
263
|
+
errors.push('COMPRESSION_LEVEL must be between 0 and 9');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this.hasRemoteConfig()) {
|
|
267
|
+
if (!this.config.REMOTE_PASSWORD && !this.config.REMOTE_PRIVATE_KEY_PATH) {
|
|
268
|
+
warnings.push('Remote configuration missing both password and private key path');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
isValid: errors.length === 0,
|
|
274
|
+
errors,
|
|
275
|
+
warnings
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get a specific config value
|
|
281
|
+
*/
|
|
282
|
+
get(key) {
|
|
283
|
+
return this.config[key];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Set a specific config value
|
|
288
|
+
*/
|
|
289
|
+
set(key, value) {
|
|
290
|
+
this.config[key] = value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get all config as a plain object
|
|
295
|
+
*/
|
|
296
|
+
toJSON() {
|
|
297
|
+
return { ...this.config };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export { ConfigManager };
|