Full-featured file browser with dual-pane layout, upload, clipboard operations, view modes, and a complete filesystem API for programmatic control.
<iframe id="fm-demo" src="/online/webapp/file-manager" width="100%" height="500" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('fm-demo', 'fmSocket');</script>
<iframe src="https://sgapps.io/online/webapp/file-manager"
width="100%" height="600" frameborder="0"></iframe>External API filesystem operations are sandboxed to input:// only. Any attempt to read, write, navigate, or delete outside this path via the API returns an access denied error.
input:// -- sandboxed area for API operations (no auth, browser-memory-only)input:// is browser-memory-only: isolated per tab, destroyed on refresh, never touches the server/home/user/) require authentication and are not accessible via the APIThe embedder can listen for events happening inside the iframe — a file was written, a folder was removed, navigation changed, a bookmark was added, and so on. Two complementary channels:
| Channel | Covers | Subscribe via |
|---|---|---|
fs-event:on | Native fs lifecycle — every method bound on the fs instance exposes beforeX, onX, afterX events (write / remove / mv / mkdir / mkdirp / cwd / read / ls / exists / stats / …). Universal, no whitelist, works for ALL callers (external API or UI click). | socket.fire("webapp::instance::fs-event:on", "afterWrite", handler, cb) |
webapp::instance::event::<name> | Semantic / curated events that aren't native fs lifecycle hooks — gui:specialPaths:*, apps:register/unregister, fs:copy, fs:archive, fs:createShortcut, fs:bulkCopy/Remove/Archive, gui:sidebar:toggle, gui:homePath:change. Explicit emits from file-manager. | socket.on("webapp::instance::event::apps:register", handler) |
fs-event:on / off / onceAny method bound via app.bind(name, fn) inside the fs module auto-generates three lifecycle events via ApplicationPrototype:
before<Name> — synchronous, BEFORE the method body. A handler returning false can interrupt the call (when allowInterruption is on, which is the default). Use for prefetch, veto rules, instrumentation.on<Name> — synchronous, at method entry. Observe-only, by convention.after<Name> — async via setImmediate AFTER the body returns. For input:// writes (which use a Promise.resolve().then(cb) microtask) afterX fires after the user callback. For external HTTP paths it fires before the async callback — rely on the fs's own cb for completion semantics there.Handler receives the original method arguments spread: fs.on("afterWrite", function (path, data, cb) { … }) sees exactly what fs.write(path, data, cb) was called with. Don't invoke cb from a lifecycle handler — it belongs to the original caller.
Because this hooks the fs instance directly, ALL mutations fire the event, regardless of who initiated them: external API, drag-drop upload, right-click → New file, rename via the UI, decorators pressing their own save buttons (archive-viewer's "Save Archive" being the canonical example). The subscriber doesn't need to care who caused the change.
// Subscribe — handler stays alive across many fires, returns the id used by :off
socket.fire("webapp::instance::fs-event:on",
"afterWrite", // any fs lifecycle event name
function (path, data /*, cb */) { // handler
console.log("write:", path, data && (data.size || data.length));
},
function (err, identifier) { // callback — returns the id
if (err) console.error(err);
else window.__writeSubId = identifier;
}
// optional 5th arg — see "stable subscription IDs" below
);
// Unsubscribe
socket.fire("webapp::instance::fs-event:off",
"afterWrite",
window.__writeSubId,
function (err) { /* ... */ }
);
// Fire once (auto-off after first call)
socket.fire("webapp::instance::fs-event:once",
"afterRemove",
function (path) { console.log("first remove in session:", path); },
function (err, identifier) { /* ... */ }
);Stable subscription IDs — fs-event:on and fs-event:once accept an optional 5th argument that pre-chooses the subscription identifier. ApplicationPrototype's on() reuses an existing id if provided, so passing the same id twice replaces the existing listener instead of adding a second one (idempotent subscribe). Useful for embedders that want:
"my-write-watcher")"sync-engine:writes", "sync-engine:removes")socket.fire("webapp::instance::fs-event:on",
"afterWrite",
function (path, data) { /* ... */ },
function (err) { /* ... */ },
"my-write-watcher" // ← stable id; re-registering with same id replaces
);
// Later — unsubscribe by the stable id, no need to remember what the callback returned
socket.fire("webapp::instance::fs-event:off",
"afterWrite", "my-write-watcher",
function (err) { /* ... */ }
);Omit the 5th arg to let fs auto-generate one (s-N incremental).
The bridge returns fs not available on this webapp error in the callback when the hosted application doesn't expose .fs() (e.g. taskbar, chatbox — they don't use a filesystem). The embedder can test with a no-op subscribe to detect capability before setting up its UI.
Every method below is bound via app.bind(name, fn) on the fs instance — so each one emits before<Name>, on<Name>, after<Name> lifecycle events that fs-event:on can target. Replace the method name with the camelCased hook form (write → afterWrite, readRange → afterReadRange, existsFile → afterExistsFile, etc.).
Handler args passed to the hook match the method signature exactly (the trailing cb is the ORIGINAL caller's callback — don't invoke it).
Core from /files/api/modules/fs.js:
| Method | Signature | Purpose | |||
|---|---|---|---|---|---|
apiURL | (action, data) | Build the REST URL for an action | |||
request | (action, data, method) | Low-level REST request | |||
status | (path, cb) | Server status / health probe | |||
svfsConnections | (action, cb) | SVFS mount roster (list/add/remove) | |||
write | (path, data, cb) | Write a file (data = Blob / string / ArrayBuffer) | |||
append | (path, data, cb) | Append to a file | |||
touch | (path, cb) | Create empty file or bump mtime | |||
read | (path, cb, stream) | Read a file; returns a RequestModule with `.response("blob"\ | "text"\ | "json"\ | "arraybuffer")` |
readRange | (path, startPos, endPos, cb, stream) | Partial read; byte range | |||
readStreamUrl | (path) | Returns a URL usable in <img src> / <video> | |||
exists | (path, cb) | Does the path exist (file OR directory) | |||
existsFile | (path, cb) | Does the path exist AND is a file | |||
existsDirectory | (path, cb) | Does the path exist AND is a directory | |||
stats | (path, cb) | Metadata: size, mtime, permissions, mime, md5, metadata | |||
ls | (path, cb) | List directory children (one level) | |||
tree | (path, cb) | Recursive tree listing | |||
remove | (path, cb) | Delete a file | |||
mkdir | (path, cb) | Create one directory (parent must exist) | |||
mkdirp | (path, cb) | Create directory recursively | |||
rmdir | (path, cb) | Delete an empty directory | |||
mv | (path, path2, cb) | Move / rename | |||
updateMeta | (path, meta, cb) | Update metadata fields on an entry | |||
find | (path, query_filter, meta, cb) | Find entries by filter object | |||
findFile | (path, query_text, meta, cb) | Full-text file search |
config.fs.bind(...) inside file-manager.js, present only when the hosted webapp is a file-manager):| Method | Signature | Purpose |
|---|---|---|
cwd | (path?) | Getter when called without args, setter when called with a string. Setter emits afterCwd(path). |
beforeX(…methodArgs) — synchronous, before body. Return false to cancel (default allowInterruption: true).onX(…methodArgs) — synchronous, at method entry. Observe-only by convention.afterX(…methodArgs) — async via setImmediate, after body returns. For input:// paths completion lands first (microtask) then afterX fires; for external HTTP the hook can fire BEFORE the async callback.Example — spy on every ls call for a cache warmer:
socket.fire("webapp::instance::fs-event:on", "afterLs",
function (path /*, cb */) {
console.log("ls was called on:", path);
},
function (err, id) { if (err) console.error(err); },
"ls-spy"
);Example — veto writes to a protected prefix:
socket.fire("webapp::instance::fs-event:on", "beforeWrite",
function (path /*, data, cb */) {
if (/^input:\/\/\.readonly\//.test(path)) {
console.warn("blocked write:", path);
return false; // ← cancels the fs.write call
}
},
function () {},
"readonly-guard"
);Each button below subscribes to a lifecycle event on the live demo iframe, then triggers a matching request so you can see the handler fire.
Listen for afterWrite (any file write — API or UI click):
<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::fs-event:on','afterWrite',function(path,data){alert('afterWrite EVENT\nPath: '+path+'\nSize: '+((data&&(data.size||data.length))||'?')+' bytes')},function(err,id){if(err){alert('Subscribe failed: '+err.message);return}s.fire('webapp::instance::request','fs:write','input://event-demo.txt','listener fired at '+new Date().toISOString(),function(e){if(e)alert('Trigger failed: '+e.message)})})})()">Try: Subscribe + trigger write</button>
Listen for afterRemove:
<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::fs-event:on','afterRemove',function(path){alert('afterRemove EVENT\nPath: '+path)},function(err){if(err){alert(err.message);return}s.fire('webapp::instance::request','fs:write','input://event-remove-demo.txt','x',function(){s.fire('webapp::instance::request','fs:remove','input://event-remove-demo.txt')})})})()">Try: Subscribe + write + remove</button>
Listen for afterCwd (navigation):
<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::fs-event:on','afterCwd',function(path){if(typeof path==='string'&&path)alert('afterCwd EVENT\nNew path: '+path)},function(err){if(err){alert(err.message);return}s.fire('webapp::instance::request','fs:mkdirp','input://cwd-event-demo/',function(){s.fire('webapp::instance::request','fs:cwd','input://cwd-event-demo/')})})})()">Try: Subscribe + change cwd</button>
Multi-event activity feed (lifecycle + semantic together):
<div id="fm-event-feed" style="min-height:60px;max-height:200px;overflow-y:auto;padding:8px 12px;background:#0d1117;color:#d4e668;font-family:ui-monospace,monospace;font-size:11px;border-radius:4px;line-height:1.5;border:1px solid #30363d;">Click <strong>Start feed</strong>, then do any file-manager operation in the demo above (write, drag, rename, create folder, etc.).</div>
<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;var feed=document.getElementById('fm-event-feed');feed.innerHTML='<span style='color:#9ba7b4'>Listening on fs lifecycle (afterWrite/Remove/Mv/Mkdir/Mkdirp/Cwd) + semantic events…</span><br>';['afterWrite','afterRemove','afterMv','afterMkdir','afterMkdirp','afterCwd'].forEach(function(ev){s.fire('webapp::instance::fs-event:on',ev,function(){var a=Array.prototype.slice.call(arguments).filter(function(x){return typeof x!=='function'});var line=document.createElement('div');line.innerHTML='<span style='color:#58a6ff'>'+new Date().toISOString().slice(11,19)+'</span> <span style='color:#a6bb11'>fs:'+ev+'</span> '+JSON.stringify(a).slice(0,160);feed.insertBefore(line,feed.firstChild)},function(err){if(err){var line=document.createElement('div');line.innerHTML='<span style='color:#f85149'>'+ev+' subscribe failed: '+err.message+'</span>';feed.insertBefore(line,feed.firstChild)}})});['fs:copy','fs:archive','fs:createShortcut','fs:bulkCopy','fs:bulkRemove','fs:bulkArchive','gui:specialPaths:add','gui:specialPaths:remove','gui:specialPaths:clear','gui:sidebar:toggle','gui:homePath:change','apps:register','apps:unregister'].forEach(function(ev){s.on('webapp::instance::event::'+ev,function(){var a=Array.prototype.slice.call(arguments);var line=document.createElement('div');line.innerHTML='<span style='color:#58a6ff'>'+new Date().toISOString().slice(11,19)+'</span> <span style='color:#d2a8ff'>'+ev+'</span> '+JSON.stringify(a).slice(0,160);feed.insertBefore(line,feed.firstChild)})})})()">Start feed</button><button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::request','fs:mkdirp','input://feed/',function(){s.fire('webapp::instance::request','fs:write','input://feed/a.txt','hello',function(){s.fire('webapp::instance::request','fs:write','input://feed/b.json',JSON.stringify({x:1}),function(){s.fire('webapp::instance::request','fs:mv','input://feed/a.txt','input://feed/renamed.txt',function(){s.fire('webapp::instance::request','fs:remove','input://feed/b.json')})})})})})()">Trigger a mixed sequence</button>
webapp::instance::event::<name>Composite operations and GUI state changes don't map cleanly to a single fs lifecycle event (e.g. apps:register affects the window-manager library, not fs; fs:bulkCopy opens a progress-UI window that fires many internal writes). These are emitted explicitly by file-manager via win.emit("api-event", name, …args) and forwarded as webapp::instance::event::<name>.
socket.on("webapp::instance::event::apps:register", function (count) {
console.log(count + " custom apps registered");
});| Event | Payload | Fired when |
|---|---|---|
fs:copy | (src, dst) | Semantic copy via fs:copy request (also fires afterWrite(dst, …) through the fs bridge) |
fs:archive | (sourcePath, outputPath, format) | Archive built from a folder; outputPath is null when the archive was downloaded instead of written |
fs:createShortcut | (shortcutPath) | A .lnk-style shortcut file was written (also fires afterWrite) |
fs:bulkCopy | (sources[], destination) | Bulk copy with UI completed |
fs:bulkRemove | (paths[]) | Bulk delete with UI completed |
fs:bulkArchive | (sources[], outputPath, format) | Bulk archive with UI completed |
gui:sidebar:toggle | (visible) | Sidebar visibility toggled |
gui:specialPaths:add | (entry) | A bookmark was added to the sidebar |
gui:specialPaths:remove | (path) | A bookmark was removed |
gui:specialPaths:clear | () | All bookmarks were cleared |
gui:homePath:change | (path) | Home-page default was changed |
apps:register | (count) | N custom apps were registered successfully |
apps:unregister | (appName) | A previously-registered custom app was removed |
Use the fs bridge for native lifecycle, the semantic channel for everything else. afterWrite detects any file write (API, drag-drop, archive-viewer save, context-menu rename). The semantic channel surfaces composite operations that fire multiple internal fs calls but represent one user action.
Clean up subscriptions. Each fs-event:on returns an identifier; use it with fs-event:off to detach. If you forget, the handler stays live until the iframe is destroyed. For long-lived embeds that mount/unmount panels, pair every subscribe with an unsubscribe in your teardown.
Don't call the handler's cb argument. Lifecycle handlers receive the original method args spread, including the callback the fs user passed. Invoking it from the embedder's handler would resolve the caller's promise twice — read-only observation.
Readonly operations have events too. afterRead, afterLs, afterExists, afterStats fire for reads — useful for cache-busting, access logging, request counting. The semantic channel is "writes only"; the fs bridge covers reads as well.
Idempotence. afterWrite fires for every write call, even when content is identical to what was already there. Debounce on the embedder side if you need "actually changed" semantics.
Before* can interrupt. A fs-event:on handler for beforeWrite / beforeRemove / etc. that returns false cancels the operation (default allowInterruption=true). Use intentionally for veto rules; return undefined from observe-only handlers.
These events let you create, read, write, delete, rename files and folders programmatically — perfect for building custom UIs on top of the embedded file manager.
fs:cwd -- Get or Set Current DirectoryNavigate the file manager to a different directory, or get the current path.
| Arg | Type | Description |
|---|---|---|
path | string (optional) | Directory path to navigate to |
callback | function | (err, currentPath: string) |
// Navigate to input://
socket.fire("webapp::instance::request", "fs:cwd", "input://", function (err, cwd) {
console.log("Now at:", cwd);
});
// Get current directory
socket.fire("webapp::instance::request", "fs:cwd", function (err, cwd) {
console.log("Current:", cwd);
});<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:cwd','input://',function(e,p){if(e)alert('Error: '+e);else console.log('Navigated to:',p)})">Try: Navigate to input://</button><button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:cwd',function(e,p){alert('Current path: '+p)})">Try: Get Current Path</button>
fs:mkdirp -- Create Directory (with parents)Creates a directory and any missing parent directories. Works like mkdir -p.
| Arg | Type | Description |
|---|---|---|
path | string | Directory path to create |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:mkdirp", "input://my-project/src/components/", function (err) {
console.log(err || "Directories created");
});<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/src/components/',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('Created input://demo-project/src/components/')}else alert('Error: '+e)})">Try: Create demo-project/src/components/</button>
fs:write -- Write a FileCreates or overwrites a file with the given content.
| Arg | Type | Description |
|---|---|---|
path | string | File path |
content | string | File content |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:write",
"input://demo-project/README.md",
"# My Project\n\nThis is a demo project created via the API.",
function (err) { console.log(err || "File written"); }
);<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/',function(){window.fmSocket.fire('webapp::instance::request','fs:write','input://demo-project/README.md','# Demo Project\n\nCreated via the File Manager API.\n\n## Features\n- In-memory storage\n- No server required\n- Destroyed on refresh',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('README.md created!')}else alert('Error: '+e)})})">Try: Create README.md</button><button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:write','input://demo-project/src/index.js','console.log('Hello from the API!');\n\nfunction add(a, b) {\n return a + b;\n}\n\nmodule.exports = { add };',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:refresh');alert('index.js created!')}else alert('Error: '+e)})">Try: Create src/index.js</button>
fs:read -- Read a FileReads the content of a file as text.
| Arg | Type | Description |
|---|---|---|
path | string | File path |
callback | function | (err, content: string) |
socket.fire("webapp::instance::request", "fs:read", "input://demo-project/README.md",
function (err, content) {
console.log("Content:", content);
}
);<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:read','input://demo-project/README.md',function(e,c){if(c)alert('File content:\n\n'+c);else alert('Error: '+(e||'file not found'))})">Try: Read README.md</button>
fs:readBinary -- Read a File as Data URLReads the content of a file and returns a base64 data: URL — useful for binary types (images, PDFs, archives) that would be mangled by text decoding.
| Arg | Type | Description |
|---|---|---|
path | string | File path |
callback | function | (err, dataUrl: string) — data:<mime>;base64,… |
socket.fire("webapp::instance::request", "fs:readBinary", "input://logo.png",
function (err, dataUrl) {
if (!err) document.getElementById("preview").src = dataUrl;
}
);<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::request','fs:write','input://binary-demo.txt','Hello binary read',function(){s.fire('webapp::instance::request','fs:readBinary','input://binary-demo.txt',function(e,u){if(u)alert('Data URL (truncated):\n\n'+u.slice(0,120)+'…');else alert('Error: '+(e&&e.message||e))})})})()">Try: Write + read as data URL</button>
fs:ls -- List Directory ContentsReturns an array of files and folders in the specified directory.
| Arg | Type | Description |
|---|---|---|
path | string | Directory path |
callback | function | (err, files: Array<{filename, length, metadata}>) |
socket.fire("webapp::instance::request", "fs:ls", "input://demo-project/",
function (err, files) {
files.forEach(function (f) {
console.log(f.metadata._isDirectory ? "[DIR]" : "[FILE]", f.filename, f.length + " bytes");
});
}
);<button onclick="window.ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:ls','input://demo-project/',function(e,f){if(f){var msg=f.map(function(x){return (x.metadata.isDirectory?'[DIR] ':'[FILE] ')+x.filename.replace(/^.*\/(?=[^\/]+\/?$)/,'')}).join('\n');alert('Contents:\n\n'+msg)}else alert('Error: '+(e||'not found'))})">Try: List demo-project/</button>
fs:mv -- Move or RenameMoves or renames a file or directory within the same protocol.
| Arg | Type | Description |
|---|---|---|
src | string | Source path |
dst | string | Destination path |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:mv",
"input://demo-project/README.md",
"input://demo-project/ABOUT.md",
function (err) { console.log(err || "Renamed"); }
);<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mv','input://demo-project/README.md','input://demo-project/ABOUT.md',function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:refresh');alert('Renamed README.md to ABOUT.md')}else alert('Error: '+e)})">Try: Rename README.md to ABOUT.md</button>
fs:remove -- Delete a FileRemoves a single file or empty directory. Lightweight, no UI.
| Arg | Type | Description |
|---|---|---|
path | string | Path to remove |
options | object (optional) | { autoConfirm: true } — accepted for API consistency |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:remove", "input://demo-project/ABOUT.md",
function (err) { console.log(err || "Deleted"); }
);
// With options
socket.fire("webapp::instance::request", "fs:remove", "input://temp/file.txt",
{ autoConfirm: true },
function (err) { console.log(err || "Deleted"); }
);fs:copy -- Copy a FileCopies a single file. Reads source as blob, writes to destination. Lightweight, no UI.
| Arg | Type | Description |
|---|---|---|
src | string | Source file path |
dst | string | Destination file path |
options | object (optional) | { autoConfirm: true } — accepted for API consistency |
callback | function | (err) |
// Simple copy
socket.fire("webapp::instance::request", "fs:copy",
"input://project/config.json",
"input://backup/config.json",
function (err) { console.log(err || "Copied"); }
);
// With options (accepted, currently same behavior)
socket.fire("webapp::instance::request", "fs:copy",
"input://project/config.json",
"input://backup/config.json",
{ autoConfirm: true },
function (err) { console.log(err || "Copied"); }
);fs:archive -- Create Archive (no UI)Creates an archive from a directory using Packer. No window, no progress UI — runs silently in background.
| Arg | Type | Description |
|---|---|---|
sourcePath | string | Directory to archive |
outputPath | string or null | Where to save (null = returns blob URL in callback) |
format | string | "zip", "tar", "tar.gz", "7z" (default: "zip") |
options | object (optional) | { autoConfirm: true } — accepted for API consistency |
callback | function | (err, result: { size, path?, url? }) |
// Archive and save to filesystem
socket.fire("webapp::instance::request", "fs:archive",
"input://project/", "input://project.zip", "zip",
function (err, result) {
console.log("Archive:", result.size, "bytes");
}
);
// Archive without saving (get blob URL for download)
socket.fire("webapp::instance::request", "fs:archive",
"input://project/", null, "tar.gz",
function (err, result) {
console.log("Blob URL:", result.url);
}
);These open a bin app window with progress bars, error handling, and conflict resolution. Use for batch operations or when you want the user to see progress.
fs:bulkCopy| Arg | Type | Description |
|---|---|---|
sources | string or Array | Source path(s) |
destination | string | Destination directory |
options | object (optional) | { autoConfirm: true } to auto-resolve conflicts |
callback | function | (err) — fires when bin/cp window closes |
socket.fire("webapp::instance::request", "fs:bulkCopy",
["input://src/file1.txt", "input://src/file2.txt"],
"input://backup/",
{ autoConfirm: true },
function (err) { console.log(err || "Bulk copy done"); }
);fs:bulkRemove| Arg | Type | Description |
|---|---|---|
paths | string or Array | Path(s) to delete |
options | object (optional) | { autoConfirm: true } to skip confirmation |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:bulkRemove",
["input://old-dir/", "input://temp.txt"],
{ autoConfirm: true },
function (err) { console.log(err || "Bulk delete done"); }
);fs:bulkArchive| Arg | Type | Description |
|---|---|---|
sources | string or Array | Path(s) to archive |
outputPath | string or null | Where to save (null = download) |
format | string | "zip", "tar", "tar.gz", "7z" |
options | object (optional) | { autoConfirm: true } to skip format selection |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:bulkArchive",
"input://project/", "input://project.zip", "zip",
{ autoConfirm: true },
function (err) { console.log(err || "Bulk archive done"); }
);When to use which: Usefs:copy,fs:remove,fs:archivefor single-file operations or scripted pipelines. Usefs:bulkCopy,fs:bulkRemove,fs:bulkArchivewhen you want the user to see progress, resolve conflicts, or when operating on many files at once.
fs:exists -- Check if Path Exists| Arg | Type | Description |
|---|---|---|
path | string | Path to check |
callback | function | (err, exists: boolean) |
socket.fire("webapp::instance::request", "fs:exists", "input://demo-project/",
function (err, exists) { console.log("Exists:", exists); }
);fs:stats -- Get File/Directory Info| Arg | Type | Description |
|---|---|---|
path | string | Path to inspect |
callback | function | (err, stats: object) |
fs:readStreamUrl -- Get Streaming URLReturns a URL that can be used in <img>, <video>, <a href> etc. For input:// files, this returns a blob: URL.
| Arg | Type | Description |
|---|---|---|
path | string | File path |
callback | function | (err, url: string) |
socket.fire("webapp::instance::request", "fs:readStreamUrl", "input://clip.mp4",
function (err, url) {
var v = document.getElementById("player");
if (!err) v.src = url;
}
);fs:download -- Trigger a Browser DownloadReads a file and triggers the browser's native download dialog. The parent page sees a file-save prompt as if the user clicked a <a download> link.
| Arg | Type | Description |
|---|---|---|
path | string | File path to download |
filename | string? | Optional download filename; defaults to the basename of path |
callback | function | (err) |
socket.fire("webapp::instance::request", "fs:download",
"input://report.pdf",
"quarterly-report.pdf",
function (err) { if (err) console.error(err); }
);<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::request','fs:write','input://download-demo.txt','Downloaded via API at '+new Date().toISOString(),function(err){if(err){alert('Setup failed: '+err.message);return}s.fire('webapp::instance::request','fs:download','input://download-demo.txt','api-download-demo.txt',function(e2){if(e2)alert('Download failed: '+e2.message)})})})()">Try: Write + download</button>
fs:refresh -- Refresh ViewRefreshes the file manager's current directory listing.
socket.fire("webapp::instance::request", "fs:refresh");gui:sidebar:toggle -- Show or Hide SidebarToggle the sidebar panel visibility. Pass false to hide, true to show.
| Arg | Type | Description |
|---|---|---|
visible | boolean | true to show, false to hide |
callback | function | (err) |
socket.fire("webapp::instance::request", "gui:sidebar:toggle", false); // hide
socket.fire("webapp::instance::request", "gui:sidebar:toggle", true); // show<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:sidebar:toggle',false)">Try: Hide Sidebar</button><button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:sidebar:toggle',true)">Try: Show Sidebar</button>
gui:specialPaths:add -- Add Custom Path to SidebarAdds a named bookmark to the path bar / sidebar. Useful for creating labeled shortcuts to directories within your sandbox. These entries are tagged as __apiManaged and can be removed later.
| Arg | Type | Description |
|---|---|---|
entry | object | { path, name, icon?, description?, paths?, bookmark? } |
callback | function | (err) |
The icon field supports three formats:
| Format | Example | Description |
|---|---|---|
| Icon library name | "breeze/icons/places/64/folder-blue" | Resolved via Application.icon() to /scripts/modules/icons/... |
| Absolute URL | "https://example.com/icon.png" | Used as-is (also http://, //, data:) |
| Wildcard match | "paper*folder" | * matches any path segment in the icon library |
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
path: "input://my-project/",
name: "My Project",
icon: "breeze/icons/places/64/folder-blue", // icon library name
description: "Project files",
bookmark: {
category: "personal",
name: "My Project",
icon: "breeze/icons/places/64/folder-blue"
}
}, function (err) { console.log(err || "Path added"); });
// Using an external icon URL
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
path: "input://uploads/",
name: "Uploads",
icon: "https://example.com/my-custom-icon.svg", // absolute URL
bookmark: { category: "personal", name: "Uploads", icon: "https://example.com/my-custom-icon.svg" }
});<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:add',{path:'input://demo-project/',name:'Demo Project',description:'Files created via API',bookmark:{category:'personal',name:'Demo Project'}},function(e){alert(e||'Special path added! Check the sidebar.')})">Try: Add "Demo Project" to Sidebar</button>
gui:specialPaths:remove -- Remove Custom PathRemoves a previously added API-managed special path by its path string.
| Arg | Type | Description |
|---|---|---|
path | string | The path that was added |
callback | function | (err) |
socket.fire("webapp::instance::request", "gui:specialPaths:remove",
"input://my-project/",
function (err) { console.log(err || "Removed"); }
);<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:remove','input://demo-project/',function(e){alert(e||'Special path removed')})">Try: Remove "Demo Project" from Sidebar</button>
gui:specialPaths:clear -- Clear All Sidebar BookmarksRemoves all existing bookmarks from the sidebar (both built-in and API-managed). Useful when you want to set up a clean custom sidebar for your embedded widget.
// Clear everything, then add your own bookmarks
socket.fire("webapp::instance::request", "gui:specialPaths:clear", function () {
socket.fire("webapp::instance::request", "gui:specialPaths:add", {
path: "input://my-app/",
name: "My App Files",
icon: "breeze/icons/places/64/folder-blue",
bookmark: { category: "project", name: "My App Files", icon: "breeze/icons/places/64/folder-blue" }
});
});<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:clear',function(e){alert(e||'Sidebar cleared!')})">Try: Clear All Sidebar Bookmarks</button>
Special paths can include a paths array with sub-rules that group multiple directory segments into a single named breadcrumb. This is how the navigation bar shows "My Project" instead of input:// / demo-project /.
Each sub-rule has:
rule -- regex pattern matching the remaining path after the rootname -- display name (supports {param-1}, {param-2}, etc. from regex capture groups)icon -- optional icon for this breadcrumb segmentsocket.fire("webapp::instance::request", "gui:specialPaths:add", {
path: "input://demo-project/",
name: "Demo Project",
icon: "/scripts/modules/icons/breeze/icons/places/64/folder-red.svg",
// Sub-path grouping rules for the breadcrumb bar
paths: [
{
rule: "src\\/",
name: "Source Code",
icon: "/scripts/modules/icons/breeze/icons/places/64/folder-green.svg"
},
{
rule: "test\\/",
name: "Tests"
},
{
rule: "docs\\/",
name: "Documentation"
}
],
bookmark: {
category: "personal",
name: "Demo Project",
icon: "breeze/icons/places/64/folder-blue"
}
});With this configuration, the breadcrumb bar shows:
input://demo-project/ -> Demo Projectinput://demo-project/src/ -> Demo Project / Source Codeinput://demo-project/src/components/ -> Demo Project / Source Code / componentsinput://demo-project/test/ -> Demo Project / TestsWithout the paths rules, it would show: Demo Project / src / components (each dir is a separate breadcrumb).
<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:specialPaths:clear',function(){window.fmSocket.fire('webapp::instance::request','gui:specialPaths:add',{path:'input://demo-project/',name:'Demo Project',icon:'/scripts/modules/icons/breeze/icons/places/64/folder-red.svg',paths:[{rule:'src\/',name:'Source Code', icon:'/scripts/modules/icons/breeze/icons/places/64/folder-green.svg'},{rule:'test\/',name:'Tests'},{rule:'docs\/',name:'Documentation'}],bookmark:{category:'project',name:'Demo Project',icon:'breeze/icons/places/64/folder-blue'}},function(e){if(!e){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/src/components/',function(){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/test/',function(){window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://demo-project/docs/',function(){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://demo-project/');alert('Project created with grouped breadcrumbs! Navigate to src/, test/, docs/ to see the named breadcrumbs.')})})})}})})">Try: Create Project with Grouped Breadcrumbs</button>
Register your own applications that open files by MIME type. Apps can be loaded from URLs, blob URLs, or inline data URIs. See the full guide: Building Custom Apps.
apps:register -- Register Custom Appssocket.fire("webapp::instance::request", "apps:register", {
"my-viewer": {
path: "https://example.com/apps/viewer.js",
name: "My Viewer",
icon: "paper/icons/apps/accessories-text-editor",
comment: "Custom text viewer",
mimetypes: ["text/plain", "text/csv"],
args: ["%U"]
}
}, function (err, registered) {
console.log("Registered:", registered);
});var appCode = ';((' + (function () {
module.exports = function () {
var app = new ApplicationPrototype();
var node = document.createElement("div");
var win = null;
var ready = new Application.Promise();
app.bind("node", function () { return node; }, "");
app.bind("window", function () { return win; }, "");
app.bind("ready", function () { return ready; });
app.bind("handleWindow", function (w) {
win = w; w.emit("api-request::attached", []);
w.on("api-request::ready", function (cb) { cb(ready); });
});
app.bind("render", function (cb) {
win.title("Hello App");
win.height(300); win.width(400);
node.innerHTML = "<div style='padding:20px;text-align:center'>" +
"<h2>Hello!</h2><p>Args: " + (app.window().env().args || []).join(", ") + "</p></div>";
cb();
});
app.bind("destroy", function () {});
app.bind("init", function () {
app.render(function (err) {
if (err) return ready.reject(err);
ready.resolve(app);
});
return app;
});
return app;
};
}).toString() + ')());';
socket.fire("webapp::instance::request", "apps:register", {
"hello-app": {
path: "data:application/javascript;base64," + btoa(appCode),
name: "Hello App",
icon: "paper/icons/apps/system-software-install",
mimetypes: [],
args: []
}
});apps:unregister -- Remove a Registered App| Arg | Type | Description |
|---|---|---|
appName | string | Name of the app to remove |
callback | function | (err) |
disableDefaultApps | boolean | If true, allows removing built-in apps too (not just API-injected ones) |
__apiInjected: true) can be unregistered. Pass true as the third argument to also remove built-in apps like code-editor, photo-editor, etc. This is useful when you want to fully customize the app environment.// Remove a custom app
socket.fire("webapp::instance::request", "apps:unregister", "my-viewer", function (err) {
console.log(err || "Removed");
});
// Remove a built-in app (e.g., disable the default code editor)
socket.fire("webapp::instance::request", "apps:unregister", "code-editor", function (err) {
console.log(err || "Removed");
}, true);
// Replace a built-in app with your own
socket.fire("webapp::instance::request", "apps:unregister", "code-editor", function () {
socket.fire("webapp::instance::request", "apps:register", {
"code-editor": {
path: "input://apps/my-editor.js",
name: "My Code Editor",
icon: "paper/icons/apps/accessories-text-editor",
mimetypes: ["text/plain", "application/x-javascript", "text/html", "text/css"],
args: ["%U"]
}
});
}, true);apps:list -- List All Registered Appssocket.fire("webapp::instance::request", "apps:list", function (err, apps) {
Object.keys(apps).forEach(function (name) {
var app = apps[name];
console.log(name, app.__apiInjected ? "(custom)" : "(built-in)", app.name);
});
});mimetypes:register -- Register MIME Types or File HandlersAssociate file extensions with apps already in the window-manager library. Two input shapes:
| Shape | Behaviour |
|---|---|
{ ".ext": ["mime/type", …] } | Adds the given mimetypes to every API-injected app (apps registered via apps:register). Useful when you want several custom apps to all claim the same mimetype. |
{ ".ext": "appName" } | Adds a \.ext$ file-handler regex to that specific app in the library — the ext becomes a double-click target for that app. |
| Arg | Type | Description |
|---|---|---|
mappings | object | Extension-indexed map in either shape above |
callback | function | (err) |
// Shape 1 — associate .foo files with all injected apps
socket.fire("webapp::instance::request", "mimetypes:register",
{ ".foo": ["application/x-foo"] },
function (err) { console.log(err || "mimes added"); }
);
// Shape 2 — make .md open with the code-editor
socket.fire("webapp::instance::request", "mimetypes:register",
{ ".md": "code-editor" },
function (err) { console.log(err || ".md → code-editor"); }
);<button onclick="(function(){var s=window._ws('fmSocket');if(!s)return;s.fire('webapp::instance::request','mimetypes:register',{'.md':'code-editor'},function(err){alert(err?('Register failed: '+err.message):'.md files will now open with code-editor on double-click. Scroll into the demo and try /input://demo-project/README.md')})})()">Try: .md → code-editor</button>
Create clickable shortcut files that launch any app with predefined arguments. Double-clicking a .lnk file reads its JSON content and opens the specified app.
fs:createShortcut -- Create a Shortcut| Arg | Type | Description |
|---|---|---|
path | string | File path for the shortcut (must end in .lnk, must be input://) |
config | object | { app, args?, name?, icon? } |
callback | function | (err) |
config fields:| Field | Type | Description |
|---|---|---|
app | string | App name from the registry (e.g., "photo-editor", "code-editor", or a custom app name) |
args | array | Arguments to pass to the app (optional, default []) |
name | string | Display name (optional) |
icon | string | Icon name (optional) |
// Create a shortcut to the Photo Editor
socket.fire("webapp::instance::request", "fs:createShortcut",
"input://desktop/Photo Editor.lnk",
{
app: "photo-editor",
args: [],
name: "Photo Editor",
icon: "paper/*/multimedia-photo-viewer.png"
},
function (err) { console.log(err || "Shortcut created"); }
);
// Create a shortcut that opens a specific file in Code Editor
socket.fire("webapp::instance::request", "fs:createShortcut",
"input://desktop/Edit Config.lnk",
{
app: "code-editor",
args: ["input://project/config.json"],
name: "Edit Config"
}
);
// Create a shortcut to a custom registered app
socket.fire("webapp::instance::request", "fs:createShortcut",
"input://desktop/View Logs.lnk",
{
app: "log-viewer",
args: ["input://logs/server.log"],
name: "Server Logs"
}
);fs:createShortcut writes a JSON file at the given path:
`json
{
"__shortcut": true,
"app": "photo-editor",
"args": [],
"name": "Photo Editor",
"icon": "paper/*/multimedia-photo-viewer.png"
}
`.lnk files via file-handlers.lnk file, xdg-open matches it to the shortcut runner, which:
- Reads the JSON content
- Extracts app and args
- Calls windowManager.open({ run: [app].concat(args) })
- Closes itself<button onclick="window._ws('fmSocket')&&(function(s){s.fire('webapp::instance::request','fs:mkdirp','input://shortcuts/',function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Photo Editor.lnk',{app:'photo-editor',args:[],name:'Photo Editor',icon:'paper//multimedia-photo-viewer.png'},function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Code Editor.lnk',{app:'code-editor',args:[],name:'Code Editor',icon:'breezebook-edit'},function(){s.fire('webapp::instance::request','fs:createShortcut','input://shortcuts/Open Recorder.lnk',{app:'recorder',args:[],name:'Recorder',icon:'breeze/icons/actions/16/media-record'},function(){s.fire('webapp::instance::request','fs:cwd','input://shortcuts/',function(){alert('Shortcuts created! Double-click any .lnk file to launch the app.')})})})})})})(window.fmSocket)">Try: Create App Shortcuts</button>
This creates 3 shortcuts in input://shortcuts/:
Double-click any of them to launch the corresponding app in a new window.
gui:setHomePath -- Set Default Start DirectorySets the starting directory when the file manager opens or when navigating "home".
| Arg | Type | Description |
|---|---|---|
path | string | Directory path (must be input://) |
callback | function | (err) |
socket.fire("webapp::instance::request", "gui:setHomePath", "input://my-workspace/");<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','gui:setHomePath','input://shortcuts/',function(e){alert(e||'Home path set to input://shortcuts/')})">Try: Set Home to input://shortcuts/</button>
viewport:addFiles)Inject custom clickable icons into the file manager's current view. These are purely visual — they don't create actual files in the filesystem. Each icon can launch an app, open a URL, or show a custom context menu.
viewport:addFiles -- Add Virtual Icons to Current View| Arg | Type | Description |
|---|---|---|
path | string (optional) | Directory path to show the icons in (defaults to current directory) |
files | Array | Array of virtual file definitions |
callback | function | (err, count) |
| Field | Type | Description |
|---|---|---|
name | string | Display name (required) |
icon | string | Icon name or URL |
title | string | Tooltip text |
app | string | App to launch on double-click |
args | array | Arguments for the app |
url | string | URL to open on double-click (alternative to app) |
contextmenu | array | Custom right-click menu items: [{ text, icon, app?, args?, url? }] |
// Add app launcher icons to the current directory
socket.fire("webapp::instance::request", "viewport:addFiles", [
{
name: "Open Photo Editor",
icon: "paper/*/multimedia-photo-viewer.png",
title: "Launch the Photo Editor",
app: "photo-editor",
args: []
},
{
name: "Visit SGApps.IO",
icon: "paper/icons/apps/internet-web-browser",
title: "Open website in new tab",
url: "https://sgapps.io"
},
{
name: "Project Tools",
icon: "breeze/icons/categories/32/applications-development",
title: "Development tools",
app: "code-editor",
contextmenu: [
{ text: "Open Code Editor", icon: "breeze*book-edit", app: "code-editor" },
{ text: "Open Photo Editor", icon: "paper*multimedia-photo-viewer", app: "photo-editor" },
{ text: "Open Recorder", icon: "breeze/icons/actions/16/media-record", app: "recorder" }
]
}
]);
// Add icons to a specific path
socket.fire("webapp::instance::request", "viewport:addFiles",
"input://my-folder/",
[{ name: "My App", icon: "paper/icons/apps/system-software-install", app: "my-custom-app" }]
);Virtual icons are:
<button onclick="window._ws('fmSocket')&&window.fmSocket.fire('webapp::instance::request','fs:mkdirp','input://desktop/',function(){window.fmSocket.fire('webapp::instance::request','viewport:addFiles','input://desktop/',[{name:'Photo Editor',icon:'paper//multimedia-photo-viewer.png',title:'Launch Photo Editor',app:'photo-editor',args:[]},{name:'Code Editor',icon:'breezebook-edit',title:'Launch Code Editor',app:'code-editor',args:[]},{name:'Recorder',icon:'breeze/icons/actions/16/media-record',title:'Launch Recorder',app:'recorder',args:[]},{name:'SGApps Website',icon:'paper/icons/apps/internet-web-browser',title:'Open sgapps.io',url:'https://sgapps.io'},{name:'Tools',icon:'breeze/icons/categories/32/applications-development',title:'Dev tools menu',app:'code-editor',contextmenu:[{text:'Code Editor',icon:'breezebook-edit',app:'code-editor'},{text:'Photo Editor',icon:'papermultimedia-photo-viewer',app:'photo-editor'},{text:'File Manager',icon:'breeze/icons/apps/48/system-file-manager',app:'file-manager'}]}],function(e,n){window.fmSocket.fire('webapp::instance::request','fs:cwd','input://desktop/');alert(e||('Added '+n+' virtual icons to desktop! Double-click any icon to launch the app. Right-click Tools for a menu.'))})})">Try: Add App Icons to Desktop</button>
This example creates a complete project with multiple files and directories using only the API:
<!DOCTYPE html>
<html>
<head><title>File Manager API Demo</title></head>
<body>
<div style="display:flex;gap:8px;padding:10px;flex-wrap:wrap;">
<button id="create-project">Create Project</button>
<button id="list-files">List Files</button>
<button id="read-file">Read package.json</button>
</div>
<iframe id="fm" src="https://sgapps.io/online/webapp/file-manager"
width="100%" height="500" frameborder="0"></iframe>
<pre id="output" style="background:#f6f8fa;padding:10px;max-height:200px;overflow:auto;"></pre>
<script src="https://sgapps.io/components/application-prototype/ApplicationPrototype.js"></script>
<script src="https://sgapps.io/components/window-socket/index.js"></script>
<script>
var socket = new WindowSocket();
socket.start();
var ready = false;
socket.on("webapp::connection::ping", function () { ready = true; });
function req(event) {
var args = Array.prototype.slice.call(arguments, 1);
return new Promise(function (resolve, reject) {
args.push(function (err, result) {
if (err) reject(err); else resolve(result);
});
socket.fire.apply(socket, ["webapp::instance::request", event].concat(args));
});
}
document.getElementById('create-project').onclick = async function () {
if (!ready) return alert("Not ready yet");
await req("fs:mkdirp", "input://my-app/src/");
await req("fs:mkdirp", "input://my-app/test/");
await req("fs:write", "input://my-app/package.json",
JSON.stringify({ name: "my-app", version: "1.0.0", main: "src/index.js" }, null, 2));
await req("fs:write", "input://my-app/src/index.js",
"module.exports = function greet(name) {\n return 'Hello, ' + name + '!';\n};");
await req("fs:write", "input://my-app/test/test.js",
"var greet = require('../src/index');\nconsole.log(greet('World'));");
await req("fs:write", "input://my-app/README.md",
"# my-app\n\nA demo project created via the File Manager API.");
await req("fs:cwd", "input://my-app/");
document.getElementById('output').textContent = "Project created!";
};
document.getElementById('list-files').onclick = async function () {
if (!ready) return;
var files = await req("fs:ls", "input://my-app/");
document.getElementById('output').textContent = files.map(function (f) {
return (f.metadata._isDirectory ? "[DIR] " : "[FILE] ") + f.filename;
}).join("\n");
};
document.getElementById('read-file').onclick = async function () {
if (!ready) return;
var content = await req("fs:read", "input://my-app/package.json");
document.getElementById('output').textContent = content;
};
</script>
</body>
</html>input://) for temporary files