@ecmaos/coreutils 0.3.0 → 0.4.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +68 -0
- package/dist/commands/cron.d.ts +4 -0
- package/dist/commands/cron.d.ts.map +1 -0
- package/dist/commands/cron.js +532 -0
- package/dist/commands/cron.js.map +1 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +129 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +184 -77
- package/dist/commands/head.js.map +1 -1
- package/dist/commands/less.d.ts.map +1 -1
- package/dist/commands/less.js +1 -0
- package/dist/commands/less.js.map +1 -1
- package/dist/commands/man.d.ts.map +1 -1
- package/dist/commands/man.js +13 -1
- package/dist/commands/man.js.map +1 -1
- package/dist/commands/mount.d.ts +4 -0
- package/dist/commands/mount.d.ts.map +1 -0
- package/dist/commands/mount.js +1041 -0
- package/dist/commands/mount.js.map +1 -0
- package/dist/commands/open.d.ts +4 -0
- package/dist/commands/open.d.ts.map +1 -0
- package/dist/commands/open.js +74 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/play.d.ts +4 -0
- package/dist/commands/play.d.ts.map +1 -0
- package/dist/commands/play.js +231 -0
- package/dist/commands/play.js.map +1 -0
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +67 -17
- package/dist/commands/tar.js.map +1 -1
- package/dist/commands/umount.d.ts +4 -0
- package/dist/commands/umount.d.ts.map +1 -0
- package/dist/commands/umount.js +104 -0
- package/dist/commands/umount.js.map +1 -0
- package/dist/commands/video.d.ts +4 -0
- package/dist/commands/video.d.ts.map +1 -0
- package/dist/commands/video.js +250 -0
- package/dist/commands/video.js.map +1 -0
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.d.ts.map +1 -0
- package/dist/commands/view.js +830 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +348 -0
- package/dist/commands/web.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
- package/src/commands/cron.ts +591 -0
- package/src/commands/env.ts +143 -0
- package/src/commands/head.ts +176 -77
- package/src/commands/less.ts +1 -0
- package/src/commands/man.ts +12 -1
- package/src/commands/mount.ts +1193 -0
- package/src/commands/open.ts +84 -0
- package/src/commands/play.ts +249 -0
- package/src/commands/tar.ts +62 -19
- package/src/commands/umount.ts +117 -0
- package/src/commands/video.ts +267 -0
- package/src/commands/view.ts +916 -0
- package/src/commands/web.ts +377 -0
- package/src/index.ts +29 -2
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Fetch, InMemory, mounts, resolveMountConfig, SingleBuffer } from '@zenfs/core';
|
|
4
|
+
import { IndexedDB, WebStorage, WebAccess, /* XML */ } from '@zenfs/dom';
|
|
5
|
+
import { Iso, Zip } from '@zenfs/archives';
|
|
6
|
+
import { Dropbox, /* S3Bucket, */ GoogleDrive } from '@zenfs/cloud';
|
|
7
|
+
import { TerminalCommand } from '../shared/terminal-command.js';
|
|
8
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js';
|
|
9
|
+
/**
|
|
10
|
+
* Parse a single fstab line
|
|
11
|
+
* @param line - The line to parse
|
|
12
|
+
* @returns Parsed entry or null if line is empty/comment
|
|
13
|
+
*/
|
|
14
|
+
function parseFstabLine(line) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
// Skip empty lines and comments
|
|
17
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Split by whitespace (space or tab)
|
|
21
|
+
// Format: source target type [options]
|
|
22
|
+
const parts = trimmed.split(/\s+/);
|
|
23
|
+
if (parts.length < 3) {
|
|
24
|
+
// Need at least source, target, and type
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const source = parts[0] || '';
|
|
28
|
+
const target = parts[1] || '';
|
|
29
|
+
const type = parts[2] || '';
|
|
30
|
+
const options = parts.slice(3).join(' ') || undefined;
|
|
31
|
+
// Validate required fields
|
|
32
|
+
if (!target || !type) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
source: source || '',
|
|
37
|
+
target,
|
|
38
|
+
type,
|
|
39
|
+
options
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse a complete fstab file
|
|
44
|
+
* @param content - The fstab file content
|
|
45
|
+
* @returns Array of parsed fstab entries
|
|
46
|
+
*/
|
|
47
|
+
function parseFstabFile(content) {
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (!line)
|
|
52
|
+
continue;
|
|
53
|
+
const parsed = parseFstabLine(line);
|
|
54
|
+
if (parsed) {
|
|
55
|
+
entries.push(parsed);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
function printUsage(process, terminal) {
|
|
61
|
+
const usage = `Usage: mount [OPTIONS] [SOURCE] TARGET
|
|
62
|
+
mount [-a|--all]
|
|
63
|
+
mount [-l|--list]
|
|
64
|
+
|
|
65
|
+
Mount a filesystem.
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
-t, --type TYPE filesystem type (fetch, indexeddb, webstorage, webaccess, memory, singlebuffer, zip, iso, dropbox, /* s3, */ googledrive)
|
|
69
|
+
-o, --options OPTS mount options (comma-separated key=value pairs)
|
|
70
|
+
-a, --all mount all filesystems listed in /etc/fstab
|
|
71
|
+
-l, --list list all mounted filesystems
|
|
72
|
+
--help display this help and exit
|
|
73
|
+
|
|
74
|
+
Filesystem types:
|
|
75
|
+
fetch mount a remote filesystem via HTTP fetch
|
|
76
|
+
indexeddb mount an IndexedDB-backed filesystem
|
|
77
|
+
webstorage mount a WebStorage-backed filesystem (localStorage or sessionStorage)
|
|
78
|
+
webaccess mount a filesystem using the File System Access API
|
|
79
|
+
memory mount an in-memory filesystem
|
|
80
|
+
singlebuffer mount a filesystem backed by a single buffer
|
|
81
|
+
zip mount a readonly filesystem from a zip archive (requires SOURCE file or URL)
|
|
82
|
+
iso mount a readonly filesystem from an ISO image (requires SOURCE file or URL)
|
|
83
|
+
dropbox mount a Dropbox filesystem (requires client configuration via -o client)
|
|
84
|
+
googledrive mount a Google Drive filesystem (requires apiKey via -o apiKey, optionally clientId for OAuth)
|
|
85
|
+
|
|
86
|
+
Mount options:
|
|
87
|
+
baseUrl=URL base URL for fetch operations (fetch type)
|
|
88
|
+
size=BYTES buffer size in bytes for singlebuffer type (default: 1048576)
|
|
89
|
+
storage=TYPE storage type for webstorage (localStorage or sessionStorage, default: localStorage)
|
|
90
|
+
client=JSON client configuration as JSON string (dropbox type)
|
|
91
|
+
apiKey=KEY Google API key (googledrive type, required)
|
|
92
|
+
clientId=ID Google OAuth client ID (googledrive type, optional)
|
|
93
|
+
scope=SCOPE OAuth scope (googledrive type, default: https://www.googleapis.com/auth/drive)
|
|
94
|
+
cacheTTL=SECONDS cache TTL in seconds for cloud backends (optional)
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
mount -t memory /mnt/tmp mount memory filesystem at /mnt/tmp
|
|
98
|
+
mount -t indexeddb mydb /mnt/db mount IndexedDB store 'mydb' at /mnt/db
|
|
99
|
+
mount -t webstorage /mnt/storage mount WebStorage filesystem using localStorage
|
|
100
|
+
mount -t webstorage /mnt/storage -o storage=sessionStorage
|
|
101
|
+
mount -t webaccess /mnt/access mount File System Access API filesystem
|
|
102
|
+
mount -t fetch /api /mnt/api mount fetch filesystem at /mnt/api
|
|
103
|
+
mount -t fetch /api /mnt/api -o baseUrl=https://example.com
|
|
104
|
+
mount -t singlebuffer /mnt/buf mount singlebuffer filesystem at /mnt/buf
|
|
105
|
+
mount -t singlebuffer /mnt/buf -o size=2097152
|
|
106
|
+
mount -t zip https://example.com/archive.zip /mnt/zip
|
|
107
|
+
mount -t zip /tmp/archive.zip /mnt/zip
|
|
108
|
+
mount -t iso https://example.com/image.iso /mnt/iso
|
|
109
|
+
mount -t iso /tmp/image.iso /mnt/iso
|
|
110
|
+
mount -t dropbox /mnt/dropbox -o client='{"accessToken":"..."}'
|
|
111
|
+
mount -t googledrive /mnt/gdrive -o apiKey=YOUR_API_KEY
|
|
112
|
+
mount -t googledrive /mnt/gdrive -o apiKey=YOUR_API_KEY,clientId=YOUR_CLIENT_ID
|
|
113
|
+
mount -l list all mounted filesystems`;
|
|
114
|
+
writelnStderr(process, terminal, usage);
|
|
115
|
+
}
|
|
116
|
+
export function createCommand(kernel, shell, terminal) {
|
|
117
|
+
return new TerminalCommand({
|
|
118
|
+
command: 'mount',
|
|
119
|
+
description: 'Mount a filesystem',
|
|
120
|
+
kernel,
|
|
121
|
+
shell,
|
|
122
|
+
terminal,
|
|
123
|
+
run: async (pid, argv) => {
|
|
124
|
+
const process = kernel.processes.get(pid);
|
|
125
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
126
|
+
printUsage(process, terminal);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
let listMode = false;
|
|
130
|
+
let allMode = false;
|
|
131
|
+
let type;
|
|
132
|
+
let options;
|
|
133
|
+
const positionalArgs = [];
|
|
134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
135
|
+
const arg = argv[i];
|
|
136
|
+
if (arg === '-l' || arg === '--list') {
|
|
137
|
+
listMode = true;
|
|
138
|
+
}
|
|
139
|
+
else if (arg === '-a' || arg === '--all') {
|
|
140
|
+
allMode = true;
|
|
141
|
+
}
|
|
142
|
+
else if (arg === '-t' || arg === '--type') {
|
|
143
|
+
if (i + 1 < argv.length) {
|
|
144
|
+
type = argv[i + 1];
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await writelnStderr(process, terminal, chalk.red('mount: option requires an argument -- \'t\''));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (arg === '-o' || arg === '--options') {
|
|
153
|
+
if (i + 1 < argv.length) {
|
|
154
|
+
options = argv[i + 1];
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await writelnStderr(process, terminal, chalk.red('mount: option requires an argument -- \'o\''));
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (arg && !arg.startsWith('-')) {
|
|
163
|
+
positionalArgs.push(arg);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (listMode || (argv.length === 0 && !allMode)) {
|
|
167
|
+
const mountList = Array.from(mounts.entries());
|
|
168
|
+
if (mountList.length === 0) {
|
|
169
|
+
await writelnStdout(process, terminal, 'No filesystems mounted.');
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
const mountRows = mountList.map(([target, mount]) => {
|
|
173
|
+
const mountObj = mount;
|
|
174
|
+
const store = mountObj.store;
|
|
175
|
+
const backendName = store?.constructor?.name || mountObj.constructor?.name || 'Unknown';
|
|
176
|
+
const metadata = mountObj.metadata?.();
|
|
177
|
+
const name = metadata?.name || backendName;
|
|
178
|
+
return {
|
|
179
|
+
target: chalk.blue(target),
|
|
180
|
+
type: chalk.gray(backendName.toLowerCase()),
|
|
181
|
+
name: chalk.gray(name)
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
for (const row of mountRows) {
|
|
185
|
+
await writelnStdout(process, terminal, `${row.target.padEnd(30)} ${row.type.padEnd(15)} ${row.name}`);
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
if (allMode) {
|
|
190
|
+
try {
|
|
191
|
+
const fstabPath = '/etc/fstab';
|
|
192
|
+
if (!(await shell.context.fs.promises.exists(fstabPath))) {
|
|
193
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: ${fstabPath} not found`));
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
const content = await shell.context.fs.promises.readFile(fstabPath, 'utf-8');
|
|
197
|
+
const entries = parseFstabFile(content);
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
await writelnStdout(process, terminal, 'No entries found in /etc/fstab');
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
await writelnStdout(process, terminal, `Mounting ${entries.length} filesystem(s) from /etc/fstab...`);
|
|
203
|
+
let successCount = 0;
|
|
204
|
+
let failCount = 0;
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
try {
|
|
207
|
+
const entryType = entry.type;
|
|
208
|
+
const entrySource = entry.source || '';
|
|
209
|
+
const entryTarget = path.resolve('/', entry.target);
|
|
210
|
+
const entryOptions = entry.options;
|
|
211
|
+
// Validate entry
|
|
212
|
+
if (!entryType) {
|
|
213
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: skipping entry for ${entryTarget}: missing type`));
|
|
214
|
+
failCount++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!entryTarget) {
|
|
218
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: skipping entry: missing target`));
|
|
219
|
+
failCount++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Check if filesystem type doesn't require source but one is provided
|
|
223
|
+
const noSourceTypes = ['memory', 'singlebuffer', 'webstorage', 'webaccess', 'xml', 'dropbox', 'googledrive'];
|
|
224
|
+
if (entrySource && noSourceTypes.includes(entryType.toLowerCase())) {
|
|
225
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: ${entryType} filesystem does not require a source, ignoring source for ${entryTarget}`));
|
|
226
|
+
}
|
|
227
|
+
// Check if filesystem type requires source but none is provided
|
|
228
|
+
const requiresSourceTypes = ['zip', 'iso', 'fetch', 'indexeddb'];
|
|
229
|
+
if (!entrySource && requiresSourceTypes.includes(entryType.toLowerCase())) {
|
|
230
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: ${entryType} filesystem requires a source`));
|
|
231
|
+
failCount++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Create target directory if needed
|
|
235
|
+
const parentDir = path.dirname(entryTarget);
|
|
236
|
+
if (parentDir !== entryTarget && !(await shell.context.fs.promises.exists(parentDir))) {
|
|
237
|
+
await shell.context.fs.promises.mkdir(parentDir, { recursive: true });
|
|
238
|
+
}
|
|
239
|
+
if (!(await shell.context.fs.promises.exists(entryTarget))) {
|
|
240
|
+
await shell.context.fs.promises.mkdir(entryTarget, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
// Parse mount options
|
|
243
|
+
const mountOptions = entryOptions?.split(',').reduce((acc, option) => {
|
|
244
|
+
const [key, value] = option.split('=');
|
|
245
|
+
if (key && value) {
|
|
246
|
+
acc[key.trim()] = value.trim();
|
|
247
|
+
}
|
|
248
|
+
return acc;
|
|
249
|
+
}, {}) || {};
|
|
250
|
+
// Perform the mount based on type
|
|
251
|
+
switch (entryType.toLowerCase()) {
|
|
252
|
+
case 'fetch': {
|
|
253
|
+
let fetchBaseUrl = mountOptions.baseUrl || '';
|
|
254
|
+
let indexUrl;
|
|
255
|
+
if (entrySource && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
|
|
256
|
+
indexUrl = entrySource;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
if (!fetchBaseUrl) {
|
|
260
|
+
throw new Error('fetch filesystem requires either a full URL as source or baseUrl option');
|
|
261
|
+
}
|
|
262
|
+
fetchBaseUrl = new URL(fetchBaseUrl).toString();
|
|
263
|
+
indexUrl = new URL(entrySource || 'index.json', fetchBaseUrl).toString();
|
|
264
|
+
}
|
|
265
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
266
|
+
backend: Fetch,
|
|
267
|
+
index: indexUrl,
|
|
268
|
+
baseUrl: fetchBaseUrl,
|
|
269
|
+
disableAsyncCache: true,
|
|
270
|
+
}));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case 'indexeddb':
|
|
274
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
275
|
+
backend: IndexedDB,
|
|
276
|
+
storeName: entrySource || entryTarget
|
|
277
|
+
}));
|
|
278
|
+
break;
|
|
279
|
+
case 'webstorage': {
|
|
280
|
+
const storageType = mountOptions.storage?.toLowerCase() || 'localstorage';
|
|
281
|
+
let storage;
|
|
282
|
+
if (storageType === 'sessionstorage') {
|
|
283
|
+
if (typeof sessionStorage === 'undefined') {
|
|
284
|
+
throw new Error('sessionStorage is not available in this environment');
|
|
285
|
+
}
|
|
286
|
+
storage = sessionStorage;
|
|
287
|
+
}
|
|
288
|
+
else if (storageType === 'localstorage') {
|
|
289
|
+
if (typeof localStorage === 'undefined') {
|
|
290
|
+
throw new Error('localStorage is not available in this environment');
|
|
291
|
+
}
|
|
292
|
+
storage = localStorage;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
throw new Error(`invalid storage type '${storageType}'. Use 'localStorage' or 'sessionStorage'`);
|
|
296
|
+
}
|
|
297
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
298
|
+
backend: WebStorage,
|
|
299
|
+
storage
|
|
300
|
+
}));
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'webaccess': {
|
|
304
|
+
if (typeof window === 'undefined') {
|
|
305
|
+
throw new Error('File System Access API is not available in this environment');
|
|
306
|
+
}
|
|
307
|
+
const win = window;
|
|
308
|
+
if (!win.showDirectoryPicker) {
|
|
309
|
+
throw new Error('File System Access API is not available in this environment');
|
|
310
|
+
}
|
|
311
|
+
// For fstab, we can't interactively pick a directory, so skip
|
|
312
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: webaccess requires interactive directory selection`));
|
|
313
|
+
failCount++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
case 'memory':
|
|
317
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
318
|
+
backend: InMemory
|
|
319
|
+
}));
|
|
320
|
+
break;
|
|
321
|
+
case 'singlebuffer': {
|
|
322
|
+
const bufferSize = mountOptions.size
|
|
323
|
+
? parseInt(mountOptions.size, 10)
|
|
324
|
+
: 1048576;
|
|
325
|
+
if (isNaN(bufferSize) || bufferSize <= 0) {
|
|
326
|
+
throw new Error('invalid buffer size for singlebuffer type');
|
|
327
|
+
}
|
|
328
|
+
let buffer;
|
|
329
|
+
try {
|
|
330
|
+
buffer = new SharedArrayBuffer(bufferSize);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
buffer = new ArrayBuffer(bufferSize);
|
|
334
|
+
}
|
|
335
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
336
|
+
backend: SingleBuffer,
|
|
337
|
+
buffer
|
|
338
|
+
}));
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'zip': {
|
|
342
|
+
if (!entrySource) {
|
|
343
|
+
throw new Error('zip filesystem requires a source file or URL');
|
|
344
|
+
}
|
|
345
|
+
let arrayBuffer;
|
|
346
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
|
|
347
|
+
const response = await fetch(entrySource);
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw new Error(`failed to fetch archive: ${response.status} ${response.statusText}`);
|
|
350
|
+
}
|
|
351
|
+
arrayBuffer = await response.arrayBuffer();
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const sourcePath = path.resolve('/', entrySource);
|
|
355
|
+
if (!(await shell.context.fs.promises.exists(sourcePath))) {
|
|
356
|
+
throw new Error(`archive file not found: ${sourcePath}`);
|
|
357
|
+
}
|
|
358
|
+
const fileData = await shell.context.fs.promises.readFile(sourcePath);
|
|
359
|
+
arrayBuffer = new Uint8Array(fileData).buffer;
|
|
360
|
+
}
|
|
361
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
362
|
+
backend: Zip,
|
|
363
|
+
data: arrayBuffer
|
|
364
|
+
}));
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'iso': {
|
|
368
|
+
if (!entrySource) {
|
|
369
|
+
throw new Error('iso filesystem requires a source file or URL');
|
|
370
|
+
}
|
|
371
|
+
let uint8Array;
|
|
372
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(entrySource)) {
|
|
373
|
+
const response = await fetch(entrySource);
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`failed to fetch ISO image: ${response.status} ${response.statusText}`);
|
|
376
|
+
}
|
|
377
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
378
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const sourcePath = path.resolve('/', entrySource);
|
|
382
|
+
if (!(await shell.context.fs.promises.exists(sourcePath))) {
|
|
383
|
+
throw new Error(`ISO image file not found: ${sourcePath}`);
|
|
384
|
+
}
|
|
385
|
+
uint8Array = await shell.context.fs.promises.readFile(sourcePath);
|
|
386
|
+
}
|
|
387
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
388
|
+
backend: Iso,
|
|
389
|
+
data: uint8Array
|
|
390
|
+
}));
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case 'dropbox': {
|
|
394
|
+
if (!mountOptions.client) {
|
|
395
|
+
throw new Error('dropbox filesystem requires client configuration');
|
|
396
|
+
}
|
|
397
|
+
let clientConfig;
|
|
398
|
+
try {
|
|
399
|
+
clientConfig = JSON.parse(mountOptions.client);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
throw new Error('invalid JSON in client option');
|
|
403
|
+
}
|
|
404
|
+
if (!clientConfig.accessToken) {
|
|
405
|
+
throw new Error('client configuration must include accessToken');
|
|
406
|
+
}
|
|
407
|
+
const dropboxModule = await import('dropbox');
|
|
408
|
+
const DropboxClient = dropboxModule.Dropbox;
|
|
409
|
+
const client = new DropboxClient(clientConfig);
|
|
410
|
+
const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined;
|
|
411
|
+
await kernel.filesystem.fsSync.mount(entryTarget, await resolveMountConfig({
|
|
412
|
+
backend: Dropbox,
|
|
413
|
+
client,
|
|
414
|
+
...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
|
|
415
|
+
}));
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'googledrive': {
|
|
419
|
+
if (typeof window === 'undefined') {
|
|
420
|
+
throw new Error('Google Drive API requires a browser environment');
|
|
421
|
+
}
|
|
422
|
+
if (!mountOptions.apiKey) {
|
|
423
|
+
throw new Error('googledrive filesystem requires apiKey option');
|
|
424
|
+
}
|
|
425
|
+
// Google Drive mounting is complex and requires interactive auth
|
|
426
|
+
// For fstab, we'll skip it with a warning
|
|
427
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: skipping ${entryTarget}: googledrive requires interactive authentication`));
|
|
428
|
+
failCount++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
default:
|
|
432
|
+
throw new Error(`unknown filesystem type '${entryType}'`);
|
|
433
|
+
}
|
|
434
|
+
const successMessage = entrySource
|
|
435
|
+
? chalk.green(`Mounted ${entryType} filesystem from ${entrySource} to ${entryTarget}`)
|
|
436
|
+
: chalk.green(`Mounted ${entryType} filesystem at ${entryTarget}`);
|
|
437
|
+
await writelnStdout(process, terminal, successMessage);
|
|
438
|
+
successCount++;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
442
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount ${entry.target}: ${errorMessage}`));
|
|
443
|
+
failCount++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
await writelnStdout(process, terminal, `\nMount summary: ${successCount} succeeded, ${failCount} failed`);
|
|
447
|
+
return failCount > 0 ? 1 : 0;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to process /etc/fstab: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (positionalArgs.length === 0) {
|
|
455
|
+
await writelnStderr(process, terminal, chalk.red('mount: missing target argument'));
|
|
456
|
+
await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.');
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
if (positionalArgs.length > 2) {
|
|
460
|
+
await writelnStderr(process, terminal, chalk.red('mount: too many arguments'));
|
|
461
|
+
await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.');
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
if (!type) {
|
|
465
|
+
await writelnStderr(process, terminal, chalk.red('mount: filesystem type must be specified'));
|
|
466
|
+
await writelnStderr(process, terminal, 'Try \'mount --help\' for more information.');
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
const source = positionalArgs.length === 2 ? positionalArgs[0] : '';
|
|
470
|
+
const targetArg = positionalArgs[positionalArgs.length - 1];
|
|
471
|
+
if (!targetArg) {
|
|
472
|
+
await writelnStderr(process, terminal, chalk.red('mount: missing target argument'));
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
const target = path.resolve(shell.cwd, targetArg);
|
|
476
|
+
if (positionalArgs.length === 2 && (type.toLowerCase() === 'memory' || type.toLowerCase() === 'singlebuffer' || type.toLowerCase() === 'webstorage' || type.toLowerCase() === 'webaccess' || type.toLowerCase() === 'xml' || type.toLowerCase() === 'dropbox' /* || type.toLowerCase() === 's3' */ || type.toLowerCase() === 'googledrive')) {
|
|
477
|
+
await writelnStderr(process, terminal, chalk.yellow(`mount: ${type.toLowerCase()} filesystem does not require a source`));
|
|
478
|
+
await writelnStderr(process, terminal, `Usage: mount -t ${type.toLowerCase()} TARGET`);
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
if (positionalArgs.length === 1 && (type.toLowerCase() === 'zip' || type.toLowerCase() === 'iso')) {
|
|
482
|
+
await writelnStderr(process, terminal, chalk.red(`mount: ${type.toLowerCase()} filesystem requires a source file or URL`));
|
|
483
|
+
await writelnStderr(process, terminal, `Usage: mount -t ${type.toLowerCase()} SOURCE TARGET`);
|
|
484
|
+
return 1;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const parentDir = path.dirname(target);
|
|
488
|
+
if (parentDir !== target && !(await shell.context.fs.promises.exists(parentDir))) {
|
|
489
|
+
await shell.context.fs.promises.mkdir(parentDir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
if (!(await shell.context.fs.promises.exists(target))) {
|
|
492
|
+
await shell.context.fs.promises.mkdir(target, { recursive: true });
|
|
493
|
+
}
|
|
494
|
+
const mountOptions = options?.split(',').reduce((acc, option) => {
|
|
495
|
+
const [key, value] = option.split('=');
|
|
496
|
+
if (key && value) {
|
|
497
|
+
acc[key.trim()] = value.trim();
|
|
498
|
+
}
|
|
499
|
+
return acc;
|
|
500
|
+
}, {}) || {};
|
|
501
|
+
switch (type.toLowerCase()) {
|
|
502
|
+
case 'fetch': {
|
|
503
|
+
let fetchBaseUrl = new URL(mountOptions.baseUrl || '').toString();
|
|
504
|
+
let indexUrl;
|
|
505
|
+
if (source && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
|
|
506
|
+
indexUrl = source;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
indexUrl = new URL(source || 'index.json', fetchBaseUrl).toString();
|
|
510
|
+
}
|
|
511
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
512
|
+
backend: Fetch,
|
|
513
|
+
index: indexUrl,
|
|
514
|
+
baseUrl: fetchBaseUrl,
|
|
515
|
+
disableAsyncCache: true,
|
|
516
|
+
}));
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'indexeddb':
|
|
520
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
521
|
+
backend: IndexedDB,
|
|
522
|
+
storeName: source || target
|
|
523
|
+
}));
|
|
524
|
+
break;
|
|
525
|
+
case 'webstorage': {
|
|
526
|
+
const storageType = mountOptions.storage?.toLowerCase() || 'localstorage';
|
|
527
|
+
let storage;
|
|
528
|
+
if (storageType === 'sessionstorage') {
|
|
529
|
+
if (typeof sessionStorage === 'undefined') {
|
|
530
|
+
await writelnStderr(process, terminal, chalk.red('mount: sessionStorage is not available in this environment'));
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
storage = sessionStorage;
|
|
534
|
+
}
|
|
535
|
+
else if (storageType === 'localstorage') {
|
|
536
|
+
if (typeof localStorage === 'undefined') {
|
|
537
|
+
await writelnStderr(process, terminal, chalk.red('mount: localStorage is not available in this environment'));
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
storage = localStorage;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
await writelnStderr(process, terminal, chalk.red(`mount: invalid storage type '${storageType}'. Use 'localStorage' or 'sessionStorage'`));
|
|
544
|
+
return 1;
|
|
545
|
+
}
|
|
546
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
547
|
+
backend: WebStorage,
|
|
548
|
+
storage
|
|
549
|
+
}));
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case 'webaccess': {
|
|
553
|
+
if (typeof window === 'undefined') {
|
|
554
|
+
await writelnStderr(process, terminal, chalk.red('mount: File System Access API is not available in this environment'));
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
const win = window;
|
|
558
|
+
if (!win.showDirectoryPicker) {
|
|
559
|
+
await writelnStderr(process, terminal, chalk.red('mount: File System Access API is not available in this environment'));
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const directoryHandle = await win.showDirectoryPicker();
|
|
564
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
565
|
+
backend: WebAccess,
|
|
566
|
+
handle: directoryHandle
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
571
|
+
await writelnStderr(process, terminal, chalk.yellow('mount: directory selection cancelled'));
|
|
572
|
+
return 1;
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
// TODO: Some more work needs to be done with the XML backend
|
|
579
|
+
// case 'xml': {
|
|
580
|
+
// if (typeof document === 'undefined') {
|
|
581
|
+
// await writelnStderr(process, terminal, chalk.red('mount: XML backend requires DOM APIs (document) which are not available in this environment'))
|
|
582
|
+
// return 1
|
|
583
|
+
// }
|
|
584
|
+
// let root: Element | undefined
|
|
585
|
+
// if (mountOptions.root) {
|
|
586
|
+
// const rootSelector = mountOptions.root
|
|
587
|
+
// const element = document.querySelector(rootSelector)
|
|
588
|
+
// if (!element) {
|
|
589
|
+
// await writelnStderr(process, terminal, chalk.yellow(`mount: root element '${rootSelector}' not found, creating new element`))
|
|
590
|
+
// root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement
|
|
591
|
+
// root.setAttribute('id', 'xmlfs-' + Math.random().toString(36).substring(2, 15))
|
|
592
|
+
// root.setAttribute('style', 'display: none')
|
|
593
|
+
// } else {
|
|
594
|
+
// root = element as Element
|
|
595
|
+
// }
|
|
596
|
+
// } else {
|
|
597
|
+
// root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement
|
|
598
|
+
// root.setAttribute('id', 'xmlfs-' + Math.random().toString(36).substring(2, 15))
|
|
599
|
+
// root.setAttribute('style', 'display: none')
|
|
600
|
+
// }
|
|
601
|
+
// if (!root) throw new Error('Failed to create root element')
|
|
602
|
+
// const rootNode = document.createElement('file')
|
|
603
|
+
// rootNode.setAttribute('paths', JSON.stringify(['/']))
|
|
604
|
+
// rootNode.setAttribute('nlink', '1')
|
|
605
|
+
// rootNode.setAttribute('mode', (constants.S_IFDIR | 0o777).toString(16))
|
|
606
|
+
// rootNode.setAttribute('uid', (0).toString(16))
|
|
607
|
+
// rootNode.setAttribute('gid', (0).toString(16))
|
|
608
|
+
// rootNode.textContent = '[]'
|
|
609
|
+
// root.appendChild(rootNode)
|
|
610
|
+
// try {
|
|
611
|
+
// const config = {
|
|
612
|
+
// backend: XML,
|
|
613
|
+
// root
|
|
614
|
+
// } as { backend: typeof XML; root: Element }
|
|
615
|
+
// document.body.appendChild(root)
|
|
616
|
+
// const mountConfig = await resolveMountConfig(config)
|
|
617
|
+
// await kernel.filesystem.fsSync.mount(target, mountConfig)
|
|
618
|
+
// } catch (error) {
|
|
619
|
+
// const errorMessage = error instanceof Error ? error.message : String(error)
|
|
620
|
+
// await writelnStderr(process, terminal, chalk.red(`mount: failed to mount XML filesystem: ${errorMessage}`))
|
|
621
|
+
// if (error instanceof Error && error.stack) {
|
|
622
|
+
// await writelnStderr(process, terminal, chalk.gray(`Stack: ${error.stack}`))
|
|
623
|
+
// }
|
|
624
|
+
// return 1
|
|
625
|
+
// }
|
|
626
|
+
// break
|
|
627
|
+
// }
|
|
628
|
+
case 'memory':
|
|
629
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
630
|
+
backend: InMemory
|
|
631
|
+
}));
|
|
632
|
+
break;
|
|
633
|
+
case 'singlebuffer': {
|
|
634
|
+
const bufferSize = mountOptions.size
|
|
635
|
+
? parseInt(mountOptions.size, 10)
|
|
636
|
+
: 1048576;
|
|
637
|
+
if (isNaN(bufferSize) || bufferSize <= 0) {
|
|
638
|
+
await writelnStderr(process, terminal, chalk.red('mount: invalid buffer size for singlebuffer type'));
|
|
639
|
+
return 1;
|
|
640
|
+
}
|
|
641
|
+
let buffer;
|
|
642
|
+
try {
|
|
643
|
+
buffer = new SharedArrayBuffer(bufferSize);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
buffer = new ArrayBuffer(bufferSize);
|
|
647
|
+
}
|
|
648
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
649
|
+
backend: SingleBuffer,
|
|
650
|
+
buffer
|
|
651
|
+
}));
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case 'zip': {
|
|
655
|
+
if (!source) {
|
|
656
|
+
await writelnStderr(process, terminal, chalk.red('mount: zip filesystem requires a source file or URL'));
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
let arrayBuffer;
|
|
660
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
|
|
661
|
+
await writelnStdout(process, terminal, chalk.gray(`Fetching archive from ${source}...`));
|
|
662
|
+
const response = await fetch(source);
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to fetch archive: ${response.status} ${response.statusText}`));
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
arrayBuffer = await response.arrayBuffer();
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
const sourcePath = path.resolve(shell.cwd, source);
|
|
671
|
+
if (!(await shell.context.fs.promises.exists(sourcePath))) {
|
|
672
|
+
await writelnStderr(process, terminal, chalk.red(`mount: archive file not found: ${sourcePath}`));
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
await writelnStdout(process, terminal, chalk.gray(`Reading archive from ${sourcePath}...`));
|
|
676
|
+
const fileData = await shell.context.fs.promises.readFile(sourcePath);
|
|
677
|
+
arrayBuffer = new Uint8Array(fileData).buffer;
|
|
678
|
+
}
|
|
679
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
680
|
+
backend: Zip,
|
|
681
|
+
data: arrayBuffer
|
|
682
|
+
}));
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
case 'iso': {
|
|
686
|
+
if (!source) {
|
|
687
|
+
await writelnStderr(process, terminal, chalk.red('mount: iso filesystem requires a source file or URL'));
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
let uint8Array;
|
|
691
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(source)) {
|
|
692
|
+
await writelnStdout(process, terminal, chalk.gray(`Fetching ISO image from ${source}...`));
|
|
693
|
+
const response = await fetch(source);
|
|
694
|
+
if (!response.ok) {
|
|
695
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to fetch ISO image: ${response.status} ${response.statusText}`));
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
699
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
const sourcePath = path.resolve(shell.cwd, source);
|
|
703
|
+
if (!(await shell.context.fs.promises.exists(sourcePath))) {
|
|
704
|
+
await writelnStderr(process, terminal, chalk.red(`mount: ISO image file not found: ${sourcePath}`));
|
|
705
|
+
return 1;
|
|
706
|
+
}
|
|
707
|
+
await writelnStdout(process, terminal, chalk.gray(`Reading ISO image from ${sourcePath}...`));
|
|
708
|
+
uint8Array = await shell.context.fs.promises.readFile(sourcePath);
|
|
709
|
+
}
|
|
710
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
711
|
+
backend: Iso,
|
|
712
|
+
data: uint8Array
|
|
713
|
+
}));
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case 'dropbox': {
|
|
717
|
+
if (!mountOptions.client) {
|
|
718
|
+
await writelnStderr(process, terminal, chalk.red('mount: dropbox filesystem requires client configuration'));
|
|
719
|
+
await writelnStderr(process, terminal, 'Usage: mount -t dropbox TARGET -o client=\'{"accessToken":"..."}\'');
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
let clientConfig;
|
|
724
|
+
try {
|
|
725
|
+
clientConfig = JSON.parse(mountOptions.client);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
await writelnStderr(process, terminal, chalk.red('mount: invalid JSON in client option'));
|
|
729
|
+
return 1;
|
|
730
|
+
}
|
|
731
|
+
if (!clientConfig.accessToken) {
|
|
732
|
+
await writelnStderr(process, terminal, chalk.red('mount: client configuration must include accessToken'));
|
|
733
|
+
return 1;
|
|
734
|
+
}
|
|
735
|
+
const dropboxModule = await import('dropbox');
|
|
736
|
+
const DropboxClient = dropboxModule.Dropbox;
|
|
737
|
+
const client = new DropboxClient(clientConfig);
|
|
738
|
+
const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined;
|
|
739
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
740
|
+
backend: Dropbox,
|
|
741
|
+
client,
|
|
742
|
+
...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount dropbox filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
747
|
+
return 1;
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
/* case 's3': {
|
|
752
|
+
if (!mountOptions.bucket) {
|
|
753
|
+
await writelnStderr(process, terminal, chalk.red('mount: s3 filesystem requires bucket option'))
|
|
754
|
+
await writelnStderr(process, terminal, 'Usage: mount -t s3 TARGET -o bucket=my-bucket')
|
|
755
|
+
return 1
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
// Start with default config
|
|
760
|
+
let clientConfigRaw: { region?: string; credentials?: { accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }; [key: string]: unknown } = {}
|
|
761
|
+
|
|
762
|
+
// Parse client config if provided
|
|
763
|
+
if (mountOptions.client) {
|
|
764
|
+
try {
|
|
765
|
+
clientConfigRaw = JSON.parse(mountOptions.client)
|
|
766
|
+
} catch {
|
|
767
|
+
await writelnStderr(process, terminal, chalk.red('mount: invalid JSON in client option'))
|
|
768
|
+
return 1
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Set region: use from config, then env var, then default
|
|
773
|
+
if (!clientConfigRaw.region) {
|
|
774
|
+
clientConfigRaw.region = shell.env.get('AWS_DEFAULT_REGION') || 'us-east-1'
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Use environment variables as defaults if credentials not provided
|
|
778
|
+
if (!clientConfigRaw.credentials) {
|
|
779
|
+
const accessKeyId = shell.env.get('AWS_ACCESS_KEY_ID')
|
|
780
|
+
const secretAccessKey = shell.env.get('AWS_SECRET_ACCESS_KEY')
|
|
781
|
+
const sessionToken = shell.env.get('AWS_SESSION_TOKEN')
|
|
782
|
+
|
|
783
|
+
if (accessKeyId && secretAccessKey) {
|
|
784
|
+
clientConfigRaw.credentials = {
|
|
785
|
+
accessKeyId,
|
|
786
|
+
secretAccessKey,
|
|
787
|
+
...(sessionToken ? { sessionToken } : {})
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
// Validate credentials if provided
|
|
792
|
+
if (!clientConfigRaw.credentials.accessKeyId || !clientConfigRaw.credentials.secretAccessKey) {
|
|
793
|
+
await writelnStderr(process, terminal, chalk.yellow('mount: credentials object should include both accessKeyId and secretAccessKey'))
|
|
794
|
+
await writelnStderr(process, terminal, 'Note: If credentials are not provided, AWS SDK will use default credential chain (env vars, IAM role, etc.)')
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Configure for browser environment if needed
|
|
799
|
+
if (typeof window !== 'undefined') {
|
|
800
|
+
// Ensure we're using fetch for browser requests
|
|
801
|
+
if (!clientConfigRaw.requestHandler) {
|
|
802
|
+
// The AWS SDK v3 uses fetch by default in browsers, but we can explicitly set it
|
|
803
|
+
// This helps ensure CORS is handled properly
|
|
804
|
+
clientConfigRaw.requestHandler = undefined // Let SDK use default browser fetch
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const s3Module = await import('@aws-sdk/client-s3')
|
|
809
|
+
const S3Client = s3Module.S3
|
|
810
|
+
const client = new S3Client(clientConfigRaw as never)
|
|
811
|
+
const bucketName = mountOptions.bucket
|
|
812
|
+
const prefix = mountOptions.prefix
|
|
813
|
+
const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
await kernel.filesystem.fsSync.mount(
|
|
817
|
+
target,
|
|
818
|
+
await resolveMountConfig({
|
|
819
|
+
backend: S3Bucket,
|
|
820
|
+
client,
|
|
821
|
+
bucketName,
|
|
822
|
+
...(prefix ? { prefix } : {}),
|
|
823
|
+
...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
|
|
824
|
+
})
|
|
825
|
+
)
|
|
826
|
+
} catch (mountError) {
|
|
827
|
+
const errorMessage = mountError instanceof Error ? mountError.message : String(mountError)
|
|
828
|
+
// Provide helpful guidance for common S3 errors
|
|
829
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount s3 filesystem: ${errorMessage}`))
|
|
830
|
+
await writelnStderr(process, terminal, chalk.yellow('\nS3 CORS configuration may be required:'))
|
|
831
|
+
await writelnStderr(process, terminal, 'For browser access, your S3 bucket needs CORS configuration:')
|
|
832
|
+
await writelnStderr(process, terminal, ' {')
|
|
833
|
+
await writelnStderr(process, terminal, ' "CORSRules": [{')
|
|
834
|
+
await writelnStderr(process, terminal, ' "AllowedOrigins": ["*"],')
|
|
835
|
+
await writelnStderr(process, terminal, ' "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],')
|
|
836
|
+
await writelnStderr(process, terminal, ' "AllowedHeaders": ["*"],')
|
|
837
|
+
await writelnStderr(process, terminal, ' "ExposeHeaders": ["ETag"],')
|
|
838
|
+
await writelnStderr(process, terminal, ' "MaxAgeSeconds": 3000')
|
|
839
|
+
await writelnStderr(process, terminal, ' }]')
|
|
840
|
+
await writelnStderr(process, terminal, ' }')
|
|
841
|
+
await writelnStderr(process, terminal, chalk.gray('\nAlso ensure your bucket policy allows the required operations.'))
|
|
842
|
+
throw mountError
|
|
843
|
+
}
|
|
844
|
+
} catch (error) {
|
|
845
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount s3 filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`))
|
|
846
|
+
if (error instanceof Error && error.stack) {
|
|
847
|
+
await writelnStderr(process, terminal, chalk.gray(error.stack))
|
|
848
|
+
}
|
|
849
|
+
return 1
|
|
850
|
+
}
|
|
851
|
+
break
|
|
852
|
+
} */
|
|
853
|
+
case 'googledrive': {
|
|
854
|
+
try {
|
|
855
|
+
if (typeof window === 'undefined') {
|
|
856
|
+
await writelnStderr(process, terminal, chalk.red('mount: Google Drive API requires a browser environment'));
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
if (!mountOptions.apiKey) {
|
|
860
|
+
await writelnStderr(process, terminal, chalk.red('mount: googledrive filesystem requires apiKey option'));
|
|
861
|
+
await writelnStderr(process, terminal, 'Usage: mount -t googledrive TARGET -o apiKey=YOUR_API_KEY');
|
|
862
|
+
return 1;
|
|
863
|
+
}
|
|
864
|
+
const win = window;
|
|
865
|
+
// Load Google API script if not already loaded
|
|
866
|
+
if (!win.gapi) {
|
|
867
|
+
await writelnStdout(process, terminal, chalk.gray('Loading Google API client library...'));
|
|
868
|
+
await new Promise((resolve, reject) => {
|
|
869
|
+
const script = document.createElement('script');
|
|
870
|
+
script.src = 'https://apis.google.com/js/api.js';
|
|
871
|
+
script.onload = () => resolve();
|
|
872
|
+
script.onerror = () => reject(new Error('Failed to load Google API script'));
|
|
873
|
+
document.head.appendChild(script);
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
// Wait for gapi to be available
|
|
877
|
+
let attempts = 0;
|
|
878
|
+
while (!win.gapi && attempts < 50) {
|
|
879
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
880
|
+
attempts++;
|
|
881
|
+
}
|
|
882
|
+
if (!win.gapi) {
|
|
883
|
+
await writelnStderr(process, terminal, chalk.red('mount: Failed to load Google API client library'));
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
886
|
+
// Initialize gapi.client
|
|
887
|
+
if (!win.gapi.client || !win.gapi.client.drive) {
|
|
888
|
+
await writelnStdout(process, terminal, chalk.gray('Initializing Google API client...'));
|
|
889
|
+
const initConfig = {
|
|
890
|
+
apiKey: mountOptions.apiKey
|
|
891
|
+
};
|
|
892
|
+
if (mountOptions.clientId) {
|
|
893
|
+
initConfig.clientId = mountOptions.clientId;
|
|
894
|
+
}
|
|
895
|
+
const scope = mountOptions.scope || 'https://www.googleapis.com/auth/drive';
|
|
896
|
+
initConfig.scope = scope;
|
|
897
|
+
initConfig.discoveryDocs = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'];
|
|
898
|
+
// Load the client module
|
|
899
|
+
await new Promise((resolve, reject) => {
|
|
900
|
+
if (!win.gapi?.load) {
|
|
901
|
+
reject(new Error('gapi.load is not available'));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
win.gapi.load('client', () => {
|
|
905
|
+
if (!win.gapi?.client?.init) {
|
|
906
|
+
reject(new Error('gapi.client.init is not available'));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
win.gapi.client.init(initConfig)
|
|
910
|
+
.then(() => resolve())
|
|
911
|
+
.catch(reject);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
// Load the Drive API
|
|
915
|
+
await new Promise((resolve, reject) => {
|
|
916
|
+
if (!win.gapi?.client?.request) {
|
|
917
|
+
reject(new Error('gapi.client.request is not available'));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// Drive API is loaded via discoveryDocs, but we need to ensure it's ready
|
|
921
|
+
// The Drive API should be available after init, but we'll wait a bit
|
|
922
|
+
setTimeout(() => {
|
|
923
|
+
const gapi = win.gapi;
|
|
924
|
+
if (gapi?.client?.drive) {
|
|
925
|
+
resolve();
|
|
926
|
+
}
|
|
927
|
+
else if (gapi?.client?.request) {
|
|
928
|
+
// Try to trigger Drive API loading by making a simple request
|
|
929
|
+
gapi.client.request({
|
|
930
|
+
path: 'https://www.googleapis.com/drive/v3/about?fields=user'
|
|
931
|
+
}).then(() => {
|
|
932
|
+
resolve();
|
|
933
|
+
}).catch(() => {
|
|
934
|
+
// Even if this fails, drive might still be available
|
|
935
|
+
if (gapi?.client?.drive) {
|
|
936
|
+
resolve();
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
reject(new Error('Failed to load Drive API'));
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
reject(new Error('gapi.client.request is not available'));
|
|
945
|
+
}
|
|
946
|
+
}, 500);
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (!win.gapi?.client?.drive) {
|
|
950
|
+
await writelnStderr(process, terminal, chalk.red('mount: Google Drive API is not available'));
|
|
951
|
+
await writelnStderr(process, terminal, 'Please ensure the Drive API is enabled in your Google Cloud project');
|
|
952
|
+
return 1;
|
|
953
|
+
}
|
|
954
|
+
// Handle OAuth authentication if clientId is provided
|
|
955
|
+
if (mountOptions.clientId) {
|
|
956
|
+
await writelnStdout(process, terminal, chalk.gray('Checking authentication status...'));
|
|
957
|
+
const driveScope = mountOptions.scope || 'https://www.googleapis.com/auth/drive';
|
|
958
|
+
// Check if user is already signed in
|
|
959
|
+
try {
|
|
960
|
+
const client = win.gapi?.client;
|
|
961
|
+
if (client?.request) {
|
|
962
|
+
await client.request({
|
|
963
|
+
path: 'https://www.googleapis.com/drive/v3/about?fields=user'
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
// User needs to authenticate
|
|
969
|
+
await writelnStdout(process, terminal, chalk.yellow('Authentication required. Please sign in to Google...'));
|
|
970
|
+
const authInstance = win.gapi.auth2;
|
|
971
|
+
if (authInstance?.getAuthInstance) {
|
|
972
|
+
const auth = authInstance.getAuthInstance();
|
|
973
|
+
await auth.signIn();
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// Fallback: try to trigger auth flow
|
|
977
|
+
await new Promise((resolve, reject) => {
|
|
978
|
+
if (!win.gapi?.load) {
|
|
979
|
+
reject(new Error('gapi.load is not available'));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
win.gapi.load('auth2', () => {
|
|
983
|
+
const auth2 = win.gapi.auth2;
|
|
984
|
+
if (auth2?.init) {
|
|
985
|
+
auth2.init({
|
|
986
|
+
client_id: mountOptions.clientId,
|
|
987
|
+
scope: driveScope
|
|
988
|
+
}).then(() => {
|
|
989
|
+
if (auth2.getAuthInstance) {
|
|
990
|
+
const auth = auth2.getAuthInstance();
|
|
991
|
+
auth.signIn().then(() => resolve()).catch(reject);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
resolve();
|
|
995
|
+
}
|
|
996
|
+
}).catch(reject);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
resolve();
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const drive = win.gapi.client.drive;
|
|
1007
|
+
const cacheTTL = mountOptions.cacheTTL ? parseInt(mountOptions.cacheTTL, 10) : undefined;
|
|
1008
|
+
await kernel.filesystem.fsSync.mount(target, await resolveMountConfig({
|
|
1009
|
+
backend: GoogleDrive,
|
|
1010
|
+
drive: drive, // gapi.client.drive type from global
|
|
1011
|
+
...(cacheTTL && !isNaN(cacheTTL) ? { cacheTTL } : {})
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount googledrive filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1016
|
+
if (error instanceof Error && error.stack) {
|
|
1017
|
+
await writelnStderr(process, terminal, chalk.gray(error.stack));
|
|
1018
|
+
}
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
default:
|
|
1024
|
+
await writelnStderr(process, terminal, chalk.red(`mount: unknown filesystem type '${type}'`));
|
|
1025
|
+
await writelnStderr(process, terminal, 'Supported types: fetch, indexeddb, webstorage, webaccess, memory, singlebuffer, zip, iso, dropbox, /* s3, */ googledrive');
|
|
1026
|
+
return 1;
|
|
1027
|
+
}
|
|
1028
|
+
const successMessage = source
|
|
1029
|
+
? chalk.green(`Mounted ${type} filesystem from ${source} to ${target}`)
|
|
1030
|
+
: chalk.green(`Mounted ${type} filesystem at ${target}`);
|
|
1031
|
+
await writelnStdout(process, terminal, successMessage);
|
|
1032
|
+
return 0;
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
await writelnStderr(process, terminal, chalk.red(`mount: failed to mount filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1036
|
+
return 1;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
//# sourceMappingURL=mount.js.map
|