File Manager

Full-featured file browser with dual-pane layout, upload, clipboard operations, view modes, and a complete filesystem API for programmatic control.

Live Demo

<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>

Embed

<iframe src="https://sgapps.io/online/webapp/file-manager"
    width="100%" height="600" frameborder="0"></iframe>

Security

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.


Event Subscription

The 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(&#x22;webapp::instance::fs-event:on&#x22;, &#x22;afterWrite&#x22;, handler, cb)
webapp::instance::event::&#x3C;name&#x3E; 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(&#x22;webapp::instance::event::apps:register&#x22;, handler)

Everything observable lands on one of these two channels. The first is raw filesystem lifecycle, the second is high-level webapp events.

Universal fs event bridge — fs-event:on / off / once

Any method bound via app.bind(name, fn) inside the fs module auto-generates three lifecycle events via ApplicationPrototype:

Handler receives the original method arguments spread: fs.on(&#x22;afterWrite&#x22;, function (path, data, cb) &#x7B; … &#x7D;) 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.

API

// 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:

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.

Methods exposed on fs

Every method below is bound via app.bind(name, fn) on the fs instance — so each one emits before&#x3C;Name&#x3E;, on&#x3C;Name&#x3E;, after&#x3C;Name&#x3E; 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 &#x3C;img src&#x3E; / &#x3C;video&#x3E;
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

File-manager extensions on fs (added by config.fs.bind(...) inside file-manager.js, present only when the hosted webapp is a file-manager):
Method Signature Purpose
cwd (path&#x3F;) Getter when called without args, setter when called with a string. Setter emits afterCwd(path).

Hook reference — ALL methods above expose the same three hooks:

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"
);

Live samples

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>

Semantic events — webapp::instance::event::&#x3C;name&#x3E;

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(&#x22;api-event&#x22;, name, …args) and forwarded as webapp::instance::event::&#x3C;name&#x3E;.

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

Patterns

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.


Filesystem Events Reference

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 Directory

Navigate 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 File

Creates 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 File

Reads 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 URL

Reads 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:&#x3C;mime&#x3E;;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 Contents

Returns an array of files and folders in the specified directory.

Arg Type Description
path string Directory path
callback function (err, files: Array&#x3C;&#x7B;filename, length, metadata&#x7D;&#x3E;)

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 Rename

Moves 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 File

Removes a single file or empty directory. Lightweight, no UI.

Arg Type Description
path string Path to remove
options object (optional) &#x7B; autoConfirm: true &#x7D; — 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 File

Copies 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) &#x7B; autoConfirm: true &#x7D; — 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 &#x22;zip&#x22;, &#x22;tar&#x22;, &#x22;tar.gz&#x22;, &#x22;7z&#x22; (default: &#x22;zip&#x22;)
options object (optional) &#x7B; autoConfirm: true &#x7D; — accepted for API consistency
callback function (err, result: &#x7B; size, path&#x3F;, url&#x3F; &#x7D;)

// 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);
    }
);

Bulk Operations (with UI)

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) &#x7B; autoConfirm: true &#x7D; 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) &#x7B; autoConfirm: true &#x7D; 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 &#x22;zip&#x22;, &#x22;tar&#x22;, &#x22;tar.gz&#x22;, &#x22;7z&#x22;
options object (optional) &#x7B; autoConfirm: true &#x7D; 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: Use fs:copy, fs:remove, fs:archive for single-file operations or scripted pipelines. Use fs:bulkCopy, fs:bulkRemove, fs:bulkArchive when 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 URL

Returns a URL that can be used in &#x3C;img&#x3E;, &#x3C;video&#x3E;, &#x3C;a href&#x3E; 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 Download

Reads a file and triggers the browser's native download dialog. The parent page sees a file-save prompt as if the user clicked a &#x3C;a download&#x3E; 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 View

Refreshes the file manager's current directory listing.

socket.fire("webapp::instance::request", "fs:refresh");

gui:sidebar:toggle -- Show or Hide Sidebar

Toggle 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 Sidebar

Adds 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 &#x7B; path, name, icon&#x3F;, description&#x3F;, paths&#x3F;, bookmark&#x3F; &#x7D;
callback function (err)

Icon Format

The icon field supports three formats:

Format Example Description
Icon library name &#x22;breeze/icons/places/64/folder-blue&#x22; Resolved via Application.icon() to /scripts/modules/icons/...
Absolute URL &#x22;https://example.com/icon.png&#x22; Used as-is (also http://, //, data:)
Wildcard match &#x22;paper*folder&#x22; * 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 Path

Removes 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 Bookmarks

Removes 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>


Path Bar Breadcrumb Grouping

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:

socket.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:

Without 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>


Custom App Injection

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 Apps

socket.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);
});

Inline App (Data URI)

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)

By default, only apps registered via the API (__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 Apps

socket.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 Handlers

Associate file extensions with apps already in the window-manager library. Two input shapes:

Shape Behaviour
&#x7B; &#x22;.ext&#x22;: [&#x22;mime/type&#x22;, …] &#x7D; 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.
&#x7B; &#x22;.ext&#x22;: &#x22;appName&#x22; &#x7D; Adds a \.ext&#x24; 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>


Shortcuts (.lnk files)

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 &#x7B; app, args&#x3F;, name&#x3F;, icon&#x3F; &#x7D;
callback function (err)

The config fields:
Field Type Description
app string App name from the registry (e.g., &#x22;photo-editor&#x22;, &#x22;code-editor&#x22;, 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"
    }
);

How It Works

  1. fs:createShortcut writes a JSON file at the given path:
    `json
    &#x7B;
    &#x22;__shortcut&#x22;: true,
    &#x22;app&#x22;: &#x22;photo-editor&#x22;,
    &#x22;args&#x22;: [],
    &#x22;name&#x22;: &#x22;Photo Editor&#x22;,
    &#x22;icon&#x22;: &#x22;paper/*/multimedia-photo-viewer.png&#x22;
    &#x7D;
    
    `
  1. A built-in shortcut runner app is auto-registered for .lnk files via file-handlers
  1. When you double-click the .lnk file, xdg-open matches it to the shortcut runner, which: - Reads the JSON content - Extracts app and args - Calls windowManager.open(&#x7B; run: [app].concat(args) &#x7D;) - Closes itself

Live Demo

<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 Directory

Sets 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>


Virtual File Icons (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)

Each file definition:
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: [&#x7B; text, icon, app&#x3F;, args&#x3F;, url&#x3F; &#x7D;]

// 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>


Complete Example: Build a Project Structure via API

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>

Features

sgapps.io