diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-25 18:02:02 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-25 18:02:02 +0200 |
| commit | 4baa484b84c5184286cac2743575251454a5ee2b (patch) | |
| tree | cb9002f93644f363d0f35831dc97e6492bbb0ed6 /modules/core/module-fs.js | |
auto: 2026-05-25_18:02 changes
Diffstat (limited to 'modules/core/module-fs.js')
| -rw-r--r-- | modules/core/module-fs.js | 445 |
1 files changed, 445 insertions, 0 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js new file mode 100644 index 0000000..c4bc786 --- /dev/null +++ b/modules/core/module-fs.js @@ -0,0 +1,445 @@ +class FileSystemModule { + constructor() { this.commands = {}; this.terminal = null; } + getHelpInfo() { + return { + 'ls': 'ls [path] [-la] — List files and directories', + 'cat': 'cat <file> — Display file contents', + 'put': 'put <file> <content> — Create or overwrite a file', + 'rm': 'rm <file> — Remove a file; use -r/-rf for directories', + 'mkdir': 'mkdir <dir> [-p] — Create a directory; -p creates parent paths', + 'rmdir': 'rmdir <dir> — Remove a directory', + 'mv': 'mv <source> <dest> — Move file or directory', + 'cp': 'cp <source> <dest> — Copy file or directory', + 'cd': 'cd <path> — Change current directory; cd / to go to root', + 'pwd': 'pwd — Print current working directory', + 'find': 'find <pattern> — Search files by name pattern', + 'grep': 'grep <pattern> [file] — Search file content for pattern', + 'head': 'head <file> [lines] — Show first N lines (default: 10)', + 'tail': 'tail <file> [lines] — Show last N lines (default: 10)', + 'sort': 'sort <file> — Sort lines of a file alphabetically', + 'wc': 'wc <file> — Count lines, words, and bytes', + 'touch': 'touch <file> — Create empty file or update timestamp', + 'tree': 'tree — Display directory structure as a tree', + 'du': 'du [path] — Estimate file/directory size', + 'upload': 'upload — Upload a file from your device', + 'download': 'download <file> — Download a file to your device', + 'vfs-export': 'vfs-export — Export the entire virtual filesystem as JSON', + 'vfs-import': 'vfs-import — Import a previously exported VFS JSON file', + }; + } + registerCommands() { + const self = this; + this.commands['ls'] = (args) => { + const target = args && args.length > 0 && !args[0].startsWith('-') ? args[0] : undefined; + const flags = (args || []).filter(a => a.startsWith('-')); + const showLong = flags.some(f => f.includes('l')); + const showHidden = flags.some(f => f.includes('a')); + const files = this.terminal.fs.ls(target); + if (!files || files.length === 0) { this.terminal.print('(empty)', 'info'); return; } + const filtered = showHidden ? files : files.filter(f => !f.name.startsWith('.')); + if (filtered.length === 0) { this.terminal.print('(no visible files, use ls -a)', 'info'); return; } + const maxName = Math.max(...filtered.map(f => f.name.length), 8); + if (showLong) { + filtered.forEach(f => { + const type = f.type === 'dir' ? 'd' : '-'; + const perms = f.type === 'dir' ? 'rwxr-xr-x' : 'rw-r--r--'; + const size = String(f.size || 0).padStart(6); + this.terminal.print(type + perms + ' darkbot ' + size + ' ' + f.name); + }); + const total = filtered.reduce((s, f) => s + (f.size || 0), 0); + this.terminal.print('total ' + filtered.length + ' (' + total + ' bytes)', 'text-muted'); + } else { + this.terminal.printClickable(filtered); + } + }; + this.commands['cat'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: cat <filename>', 'warning'); + const content = this.terminal.fs.readFile(args[0]); + if(content !== null && content !== undefined) { + if (content.length === 0) this.terminal.print('(empty file)', 'info'); + else this.terminal.print(content); + } else this.terminal.print('cat: ' + args[0] + ': No such file', 'error'); + }; + this.commands['echo'] = (args) => { + if (!args || args.length === 0) { this.terminal.print(''); return; } + const ri = args.indexOf('>'); + if (ri >= 0) { + const c = args.slice(0, ri).join(' '); const fn = args.slice(ri + 1).join(' '); + if (fn) { this.terminal.fs.writeFile(fn, c); this.terminal.print('Wrote ' + c.length + ' bytes to ' + fn, 'success'); } + return; + } + this.terminal.print(args.join(' ')); + }; + this.commands['put'] = (args) => { + if(args.length < 2) return this.terminal.print('Usage: put <filename> <content>', 'warning'); + const name = args[0]; const content = args.slice(1).join(' '); + this.terminal.fs.writeFile(name, content); + this.terminal.print('Created/updated: ' + name + ' (' + content.length + ' bytes)', 'success'); + }; + this.commands['rm'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: rm <filename>', 'warning'); + if(args[0] === '-r' || args[0] === '-rf') { + const t = args[1]; if (!t) return this.terminal.print('Usage: rm -r <dirname>', 'warning'); + if (this.terminal.fs.deleteDirRecursive(t)) this.terminal.print('Removed: ' + t, 'success'); + else this.terminal.print('rm: ' + t + ': No such file or directory', 'error'); + return; + } + if(this.terminal.fs.deleteFile(args[0])) this.terminal.print('Removed: ' + args[0], 'success'); + else this.terminal.print('rm: ' + args[0] + ': No such file', 'error'); + }; + this.commands['mkdir'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: mkdir <dirname>', 'warning'); + if (args[0] === '-p') { + if(!args[1]) return this.terminal.print('Usage: mkdir -p <path>', 'warning'); + if(this.terminal.fs.mkdirRecursive(args[1])) this.terminal.print('Created directories: ' + args[1], 'success'); + else this.terminal.print('mkdir: cannot create ' + args[1], 'error'); + return; + } + if(this.terminal.fs.mkdir(args[0])) this.terminal.print('Created directory: ' + args[0], 'success'); + else this.terminal.print('mkdir: cannot create ' + args[0] + ': File exists', 'error'); + }; + this.commands['rmdir'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: rmdir <dirname>', 'warning'); + if (this.terminal.fs.deleteDirRecursive(args[0])) this.terminal.print('Removed directory: ' + args[0], 'success'); + else this.terminal.print('rmdir: ' + args[0] + ': No such directory', 'error'); + }; + this.commands['mv'] = (args) => { + if(!args[0] || !args[1]) return this.terminal.print('Usage: mv <source> <dest>', 'warning'); + const srcDir = this.terminal.fs.readDir(args[0]); + if (srcDir !== null) { + if (this.terminal.fs.moveDir(args[0], args[1])) { + this.terminal.print('Moved directory: ' + args[0] + ' to ' + args[1], 'success'); + } else { + this.terminal.print('mv: ' + args[0] + ': failed to move directory', 'error'); + } + return; + } + if (this.terminal.fs.moveFile(args[0], args[1])) { + this.terminal.print('Moved: ' + args[0] + ' to ' + args[1], 'success'); + } else { + this.terminal.print('mv: ' + args[0] + ': No such file or directory', 'error'); + } + }; + this.commands['cp'] = (args) => { + if(!args[0] || !args[1]) return this.terminal.print('Usage: cp <source> <dest>', 'warning'); + const srcDir = this.terminal.fs.readDir(args[0]); + if (srcDir !== null) { + if (this.terminal.fs.copyDir(args[0], args[1])) { + this.terminal.print('Copied directory: ' + args[0] + ' to ' + args[1], 'success'); + } else { + this.terminal.print('cp: ' + args[0] + ': failed to copy directory', 'error'); + } + return; + } + if (this.terminal.fs.copyFile(args[0], args[1])) { + this.terminal.print('Copied: ' + args[0] + ' to ' + args[1], 'success'); + } else { + this.terminal.print('cp: ' + args[0] + ': No such file or directory', 'error'); + } + }; + this.commands['cd'] = (args) => { + if (!args[0] || args[0] === '/') { + this.terminal.fs.currentPath = []; + this.terminal.fs.saveToStorage(); + this.terminal.updatePrompt(); + return; + } + const segs = this.terminal.fs._toAbsoluteSegments(args[0]); + if (segs === null) { + this.terminal.print('cd: ' + args[0] + ': No such file or directory', 'error'); + return; + } + const dir = this.terminal.fs._resolveDir(args[0]); + if (dir) { + this.terminal.fs.currentPath = segs; + this.terminal.fs.saveToStorage(); + this.terminal.updatePrompt(); + } else { + this.terminal.print('cd: ' + args[0] + ': Not a directory', 'error'); + } + }; + this.commands['pwd'] = () => { this.terminal.print('/' + this.terminal.fs.currentPath.join('/')); }; + this.commands['find'] = (args) => { + if (!args[0]) return this.terminal.print('Usage: find <pattern>', 'warning'); + const results = this.terminal.fs.findFiles ? this.terminal.fs.findFiles(args[0].toLowerCase()) : []; + if (results.length === 0) return this.terminal.print('No matches for ' + args[0], 'info'); + results.forEach(r => this.terminal.print('./' + r)); + this.terminal.print('Found ' + results.length + ' match(es)', 'info'); + }; + this.commands['grep'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: grep <pattern> [filename]', 'warning'); + const pattern = args[0]; const target = args[1]; + if (target) { + const c = this.terminal.fs.readFile(target); + if(c === null || c === undefined) return this.terminal.print('grep: ' + target + ': No such file', 'error'); + const lines = c.split('\n'); const re = new RegExp(pattern, 'i'); + let count = 0; lines.forEach((line, i) => { if (re.test(line)) { this.terminal.print((i+1) + ': ' + line); count++; } }); + if (count === 0) this.terminal.print('(no matches)', 'info'); + else this.terminal.print(count + ' match(es)', 'text-muted'); + } else { + const dir = this.terminal.fs.getCurrentDir(); + if (!dir) return this.terminal.print('grep: cannot search', 'error'); + let total = 0; const re = new RegExp(pattern, 'i'); + Object.keys(dir.content).forEach(name => { + const node = dir.content[name]; + if (node.type === 'file') { + (node.content || '').split('\n').forEach((line, i) => { + if (re.test(line)) { this.terminal.print(name + ':' + (i+1) + ': ' + line); total++; } + }); + } + }); + if (total === 0) this.terminal.print('(no matches)', 'info'); + else this.terminal.print(total + ' total match(es)', 'text-muted'); + } + }; + this.commands['head'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: head <filename> [lines]', 'warning'); + const c = this.terminal.fs.readFile(args[0]); + if(c === null || c === undefined) return this.terminal.print('head: ' + args[0] + ': No such file', 'error'); + const n = args[1] ? parseInt(args[1]) : 10; + this.terminal.print(c.split('\n').slice(0, n).join('\n')); + }; + this.commands['tail'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: tail <filename> [lines]', 'warning'); + const c = this.terminal.fs.readFile(args[0]); + if(c === null || c === undefined) return this.terminal.print('tail: ' + args[0] + ': No such file', 'error'); + const n = args[1] ? parseInt(args[1]) : 10; + this.terminal.print(c.split('\n').slice(-n).join('\n')); + }; + this.commands['sort'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: sort <filename>', 'warning'); + const c = this.terminal.fs.readFile(args[0]); + if(c === null || c === undefined) return this.terminal.print('sort: ' + args[0] + ': No such file', 'error'); + c.split('\n').sort().forEach(l => this.terminal.print(l)); + }; + this.commands['wc'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: wc <filename>', 'warning'); + const content = this.terminal.fs.readFile(args[0]); + if(content === null || content === undefined) return this.terminal.print('wc: ' + args[0] + ': No such file', 'error'); + const lines = content.split('\n').length; + const words = content.split(/\s+/).filter(w => w).length; + this.terminal.print(lines + ' ' + words + ' ' + content.length + ' ' + args[0]); + }; + this.commands['touch'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: touch <filename>', 'warning'); + const e = this.terminal.fs.readFile(args[0]); + if (e === null || e === undefined) { + const segs = this.terminal.fs._toAbsoluteSegments(args[0]); + if (segs) { + const dirPath = segs.slice(0, -1).join('/') || '.'; + const name = segs[segs.length - 1]; + this.terminal.fs.mkdirRecursive(dirPath); + } + this.terminal.fs.writeFile(args[0], ''); + this.terminal.print('Created: ' + args[0], 'success'); + } + else { this.terminal.print('Touched: ' + args[0], 'success'); } + }; + this.commands['tree'] = () => { + const dir = this.terminal.fs.getCurrentDir(); + if (!dir) return this.terminal.print('(no directory)', 'warning'); + const lines = []; this._printTree(dir, '', lines); + lines.forEach(l => this.terminal.print(l)); + }; + this.commands['du'] = (args) => { + const t = args && args[0] ? args[0] : '.'; + if (t === '.') { + const dir = this.terminal.fs.getCurrentDir(); if (!dir) return; + let total = 0; Object.keys(dir.content).forEach(k => { total += dir.content[k].content ? dir.content[k].content.length : 0; }); + this.terminal.print(total + '\t' + t); + } else { + const c = this.terminal.fs.readFile(t); + if (c === null || c === undefined) return; + this.terminal.print(c.length + '\t' + t); + } + }; + this.commands['upload'] = () => { document.getElementById('file-upload-input').click(); }; + this.commands['download'] = (args) => { + if(!args[0]) return this.terminal.print('Usage: download <filename>', 'warning'); + const content = this.terminal.fs.readFile(args[0]); + if(content !== null && content !== undefined) { + const blob = new Blob([content], {type: 'application/octet-stream'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = args[0]; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000); + this.terminal.print('Downloaded: ' + args[0], 'success'); + } else { this.terminal.print('download: ' + args[0] + ': No such file', 'error'); } + }; + this.commands['vfs-export'] = () => { + const data = this.terminal.fs.root; + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], {type: 'application/json'}); + const sizeMB = blob.size / (1024 * 1024); + if (sizeMB > 5) { + this.terminal.print('Warning: VFS size is ' + sizeMB.toFixed(1) + ' MB — large download may take time', 'warning'); + } + const now = new Date(); + const dateStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0'); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'webx11-filesystem-' + dateStr + '.json'; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000); + this.terminal.print('Exported VFS to webx11-filesystem-' + dateStr + '.json', 'success'); + }; + + // Session commands (save/load/export/import) + this.commands['session-save'] = (args) => { + if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning'); + const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_'); + const snapshot = { + type: 'session', + savedAt: new Date().toISOString(), + fsRoot: this.terminal.fs.root, + currentPath: this.terminal.fs.currentPath, + version: 1 + }; + const filePath = '_sessions/' + name + '.json'; + this.terminal.fs.mkdirRecursive('_sessions'); + this.terminal.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2)); + this.terminal.print('Session saved: ' + name, 'success'); + }; + this.commands['session-load'] = (args) => { + if (!args[0]) return this.terminal.print('Usage: session-load <name>', 'warning'); + const filePath = '_sessions/' + args[0] + '.json'; + const content = this.terminal.fs.readFile(filePath); + if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error'); + try { + const session = JSON.parse(content); + if (!session || session.type !== 'session' || !session.fsRoot) { + return this.terminal.print('Invalid session file: ' + args[0], 'error'); + } + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + this.terminal.fs.saveToStorage(); + this.terminal.updatePrompt(); + this.terminal.print('Session loaded: ' + args[0] + ' (' + new Date(session.savedAt).toLocaleString() + ')', 'success'); + this.terminal.executeCommand('ls'); + } catch (e) { + this.terminal.print('Failed to load session: ' + e.message, 'error'); + } + }; + this.commands['session-list'] = () => { + this.terminal.fs.mkdirRecursive('_sessions'); + const files = this.terminal.fs.ls('_sessions'); + if (!files || files.length === 0) return this.terminal.print('No saved sessions', 'info'); + this.terminal.print('Saved sessions:', 'text-muted'); + files.forEach(f => { + if (f.type === 'file' && f.name.endsWith('.json')) { + try { + const content = JSON.parse(this.terminal.fs.readFile('_sessions/' + f.name)); + const label = f.name.replace('.json', ''); + const date = content.savedAt ? new Date(content.savedAt).toLocaleString() : 'unknown'; + const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0; + this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')'); + } catch (e) { + this.terminal.print(' ' + f.name.replace('.json', '') + ' (corrupt)'); + } + } + }); + }; + this.commands['session-delete'] = (args) => { + if (!args[0]) return this.terminal.print('Usage: session-delete <name>', 'warning'); + const filePath = '_sessions/' + args[0] + '.json'; + if (this.terminal.fs.deleteFile(filePath)) { + this.terminal.print('Session deleted: ' + args[0], 'success'); + } else { + this.terminal.print('Session not found: ' + args[0], 'error'); + } + }; + this.commands['session-export'] = (args) => { + if (!args[0]) return this.terminal.print('Usage: session-export <name>', 'warning'); + const filePath = '_sessions/' + args[0] + '.json'; + const content = this.terminal.fs.readFile(filePath); + if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error'); + const blob = new Blob([content], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'webx11-session-' + args[0] + '.json'; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000); + this.terminal.print('Exported session: ' + args[0], 'success'); + }; + this.commands['session-import'] = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => { + try { + const session = JSON.parse(evt.target.result); + if (!session || session.type !== 'session' || !session.fsRoot) { + this.terminal.print('Invalid session file (not a valid session export)', 'error'); + return; + } + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + this.terminal.fs.saveToStorage(); + this.terminal.updatePrompt(); + this.terminal.print('Session imported: ' + file.name + ' (' + (session.savedAt ? new Date(session.savedAt).toLocaleString() : 'unknown date') + ')', 'success'); + this.terminal.executeCommand('ls'); + } catch (err) { + this.terminal.print('Failed to import session: ' + err.message, 'error'); + } + }; + reader.readAsText(file); + }; + input.click(); + }; + this.commands['vfs-import'] = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => { + try { + const data = JSON.parse(evt.target.result); + if (!data || !data.type) { + this.terminal.print('Invalid VFS file', 'error'); + return; + } + if (this.terminal.fs.restoreFromExport(data)) { + this.terminal.updatePrompt(); + this.terminal.print('VFS restored from: ' + file.name, 'success'); + this.terminal.executeCommand('ls'); + } else { + this.terminal.print('Failed to restore VFS', 'error'); + } + } catch (err) { + this.terminal.print('Failed to import VFS: ' + err.message, 'error'); + } + }; + reader.readAsText(file); + }; + input.click(); + }; + } + _printTree(dir, prefix, lines) { + const names = Object.keys(dir.content || {}).sort(); + names.forEach((name, i) => { + const isLast = i === names.length - 1; + const node = dir.content[name]; + const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 '; + const label = node.type === 'dir' ? name + '/' : name; + lines.push(prefix + connector + label); + if (node.type === 'dir') { + this._printTree(node, prefix + (isLast ? ' ' : '\u2502 '), lines); + } + }); + } +} |