View and edit archive contents (ZIP, TAR, TAR.GZ, 7Z, RAR) in the browser. Files are extracted to in-memory storage, editable with the built-in file manager, and can be saved back.
<iframe id="av-demo" src="/online/webapp/archive-viewer" width="100%" height="500" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('av-demo', 'avSocket');</script>
<iframe src="https://sgapps.io/online/webapp/archive-viewer"
width="100%" height="600" frameborder="0"></iframe>var archiveUrl = "https://example.com/project.zip";
var src = "https://sgapps.io/online/webapp/archive-viewer/url/" + btoa(archiveUrl);When you have the archive bytes in-page (user upload, API response, generated client-side) and want to display them in an already-embedded archive-viewer window — no wm.open(), no new window. Two steps:
fs:write the bytes to a path under input:// (session-scoped in-memory FS).archive:open tells the archive-viewer to swap the current archive for the new one, extracting into its workspace.// Prerequisite: iframe embedding archive-viewer is already loaded,
// its WindowSocket is reachable as `socket` (see "Live Demo" above).
function openArchiveFromBlob(socket, blob, filename) {
var tmpPath = "input://uploads/" + (filename || "upload.zip");
// WindowSocket does not know how to serialize a Blob/File — convert to
// ArrayBuffer first (native postMessage structured-clone type).
blob.arrayBuffer().then(function (buffer) {
socket.fire("webapp::instance::request", "fs:write", tmpPath, buffer,
function (err) {
if (err) return console.error("upload failed:", err);
socket.fire("webapp::instance::request", "archive:open", tmpPath,
function (err2) {
if (err2) console.error("open failed:", err2);
else console.log("archive opened:", tmpPath);
}
);
}
);
});
}Payload type. Pass anArrayBufferfor binary (archive bytes, images, any Blob/File →blob.arrayBuffer()), or a plainstringfor text.Blob/Fileinstances cannot cross the WindowSocket bridge directly — they would arrive as empty{}on the webapp side. The receiving handler callsnew Blob([content])internally, so both ArrayBuffer and string work.
The archive:open handler is registered only when the window is launched with --archive-viewer; sending archive:open to a plain file-manager window is a no-op.
Live demo — paste any .zip into the input, then click Open:
<input type="file" id="av-swap-file" accept=".zip,.tar,.tar.gz,.tgz,.7z,.gz" style="font-size:12px"><button onclick="(function(){var el=document.getElementById('av-swap-file');var f=el&&el.files&&el.files[0];if(!f)return alert('Pick a file first');var ws=window.ws&&window.ws('avSocket');if(!ws)return alert('Socket not ready');var tmp='input://uploads/'+f.name;f.arrayBuffer().then(function(buf){ws.fire('webapp::instance::request','fs:write',tmp,buf,function(err){if(err)return alert('upload failed: '+(err&&err.message||err));ws.fire('webapp::instance::request','archive:open',tmp,function(err2){if(err2)alert('open failed: '+(err2&&err2.message||err2));else alert('Loaded '+f.name+' into the viewer above')})})},function(er){alert('read failed: '+(er&&er.message||er))})})()">Try: Upload & Open in Viewer</button>
Notes on archive:open:
Enable, disable, or read the current state of the Auto-save checkbox froman embedder — same checkbox the operator can toggle in the footer:
// Enable auto-save
socket.fire("webapp::instance::request", "archive:auto-save", true,
function (err, isOn) { console.log("auto-save:", isOn); }
);
// Disable
socket.fire("webapp::instance::request", "archive:auto-save", false,
function (err, isOn) {}
);
// Read current state without changing it
socket.fire("webapp::instance::request", "archive:auto-save", null,
function (err, isOn) { console.log("auto-save is", isOn ? "ON" : "OFF"); }
);The callback fires with (null, autoUpdate) on success. The handler isregistered only when the window is launched with --archive-viewer (samecondition as archive:open).
Enabling only flips the flag — it does not force an immediate save. Theexisting debounce (~2s after the next fs change) applies as usual. Toforce a save, make any write to the extracted tree, or call the SaveArchive button in the UI.
<button onclick="(function(){var s=window._ws('avSocket');if(!s)return;s.fire('webapp::instance::request','archive:auto-save',true,function(err,on){if(err)return alert('error: '+(err&&err.message||err));alert('Auto-save is now: '+(on?'ON':'OFF'))})})()">Try: Enable Auto-save</button><button onclick="(function(){var s=window._ws('avSocket');if(!s)return;s.fire('webapp::instance::request','archive:auto-save',false,function(err,on){if(err)return alert('error: '+(err&&err.message||err));alert('Auto-save is now: '+(on?'ON':'OFF'))})})()">Try: Disable Auto-save</button><button onclick="(function(){var s=window._ws('avSocket');if(!s)return;s.fire('webapp::instance::request','archive:auto-save',null,function(err,on){if(err)return alert('error: '+(err&&err.message||err));alert('Auto-save is currently: '+(on?'ON':'OFF'))})})()">Try: Read Auto-save State</button>
Beyond afterWrite (which fires per-file) the viewer emits coarse-grainedsemantic events for every stage of its lifecycle. Subscribe once from theembedder:
socket.on("webapp::instance::event::archive:save:done",
function (path, size, format) {
console.log("archive saved:", path, size, "bytes", "format:", format);
}
);| Event | Payload | Fires on | |
|---|---|---|---|
archive:status | (text, type) where type ∈ {"info","success","error"} | Every status-bar update — firehose for the whole lifecycle | |
archive:ready | `(path\ | null, name)` | New empty archive created (no archive path at launch) |
archive:load:start | (path, name) | Opening an archive — initial launch or archive:open | |
archive:load:done | (path, name, size, format) | Archive opened and fully extracted into the workspace | |
archive:load:error | (message, path) | Load failed (bad path, read error, extract error) | |
archive:extract:start | () | Extraction phase of a load started | |
archive:extract:progress | (idx, total) | Throttled to at most 1 event per percent — safe for huge archives | |
archive:extract:done | (entryCount, format) | All entries written to the workspace | |
archive:extract:error | (message) | Extraction failed (packer error, mkdirp error) | |
archive:save:start | `(path\ | null, format)` | Save Archive click / auto-save fire — before rebuild |
archive:save:done | `(path\ | null, size, format)` | Archive rebuilt and (if path) written back; path=null means rebuild-only (no output path configured) |
archive:save:error | (message) | Rebuild or write-back failed | |
archive:download:start | (format) | Download/Download-as click — before rebuild for export | |
archive:download:done | (filename, size, format) | File handed to the browser Downloads tray | |
archive:download:error | (message) | Pack/generate/trigger-download failed | |
archive:auto-save-changed | (state, source) where source ∈ {"api","ui","launch-arg"} | Auto-save flag flipped from any channel (see above) |
archive:save:done and afterWrite?afterWrite is fired by the fs wrapper on every write — including each
file in the extracted tree while the operator edits inside the archive,
AND the final archive rebuild. Filter by path prefix to disambiguate.archive:save:done fires once per save cycle, with structured payload
(path, size, format) and only for the archive rebuild — no false
positives from in-archive edits.Use archive:save:done for "archive was saved" semantics; use afterWritewhen you also need per-file notifications or when you want a single hookthat catches both external api writes and UI-driven in-archive edits.
<button onclick="(function(){var s=window._ws('avSocket');if(!s)return;['archive:ready','archive:load:start','archive:load:done','archive:load:error','archive:extract:start','archive:extract:done','archive:extract:error','archive:save:start','archive:save:done','archive:save:error','archive:download:start','archive:download:done','archive:download:error','archive:auto-save-changed'].forEach(function(n){s.on('webapp::instance::event::'+n,function(){console.log('[archive-event]',n,Array.prototype.slice.call(arguments))})});s.on('webapp::instance::event::archive:extract:progress',function(i,t){console.log('[archive-event] archive:extract:progress',i+'/'+t)});alert('All archive:* lifecycle listeners armed. Open the DevTools console, then upload a new archive via the input above to watch them stream.')})()">Try: Log All Archive Lifecycle Events (DevTools console)</button>
The viewer also emits a webapp::instance::event::archive:auto-save-changedevent whenever the flag flips — from either channel: an archive:auto-saveapi call, the operator clicking the footer checkbox, or the --auto-updatelaunch flag. Payload: (state, source) where source is "api", "ui",or "launch-arg".
socket.on("webapp::instance::event::archive:auto-save-changed",
function (state, source) {
console.log("auto-save →", state, "(changed via", source + ")");
// sync your embedder-side UI toggle here
}
);<button onclick="(function(){var s=window._ws('avSocket');if(!s)return;s.on('webapp::instance::event::archive:auto-save-changed',function(state,source){alert('AUTO-SAVE CHANGED\n\nNew state: '+(state?'ON':'OFF')+'\nSource: '+source)});alert('Listener armed. Toggle Auto-save in the viewer above (or click one of the buttons above) to see the event fire.')})()">Try: Listen for Auto-save Changes</button>
input:// protocol)| Format | Read | Write |
|---|---|---|
| ZIP | Yes | Yes |
| TAR | Yes | Yes |
| TAR.GZ | Yes | Yes |
| 7Z | Yes | Yes |
| RAR | Yes | No (proprietary) |
| GZIP | Yes | Yes |
The archive viewer inherits all File Manager filesystem events. Since the archive is extracted to input://, you can use these events to programmatically add, modify, or remove files in the archive.
The archive viewer rebuilds the archive on Save Archive click or when Auto-save is on. The rebuilt bytes are written back to the original archive path via the shared fs — same code path the external API uses — so subscribing to the afterWrite lifecycle event through the universal fs-event bridge catches both API writes AND the UI's save button:
// socket is the WindowSocket of the embedded archive-viewer iframe
socket.fire("webapp::instance::fs-event:on",
"afterWrite",
function (path, data /*, cb */) {
// The viewer writes BOTH:
// • the extracted tree (input://.tmp/archive-viewer/<uid>/…) — ignore
// • the ORIGINAL archive path — react here
if (/^input:\/\/\.tmp\/archive-viewer\//.test(path)) return;
var size = (data && (data.size || data.length)) || 0;
console.log("archive saved:", path, "(" + size + " bytes)");
// → trigger your sync-to-backend, update your UI, etc.
},
function (err, subscriptionId) {
if (err) console.error("subscribe failed:", err);
else window.__archiveSaveSubId = subscriptionId;
}
);
// later, when the embedding iframe is removed:
// socket.fire("webapp::instance::fs-event:off", "afterWrite",
// window.__archiveSaveSubId, function () {});<button onclick="(function(){var s=window._ws('avSocket');if(!s)return;s.fire('webapp::instance::fs-event:on','afterWrite',function(path,data){if(/^input:\/\/\.tmp\/archive-viewer\//.test(path))return;var size=(data&&(data.size||data.length))||0;alert('ARCHIVE SAVED\n\nPath: '+path+'\nNew size: '+size+' bytes\n\nUse this hook to sync the saved archive upstream.')},function(err,id){if(err)alert('Subscribe failed: '+err.message);else alert('Listener armed. Click Save Archive in the viewer above (or enable Auto-save and make a change) to see the event fire.')})})()">Try: Listen for archive save</button>
Note. afterWrite fires for both extracted-tree writes (while editing inside the archive) AND the final archive rebuild. Filter by path prefix (input://.tmp/archive-viewer/ → in-archive editing) to distinguish them, as the snippet above shows.
See File Manager → Event Subscription for the full lifecycle catalogue (beforeX / onX / afterX for every bound fs method) and the semantic events channel.
// Write a new file into the extracted archive
socket.fire("webapp::instance::request", "fs:write",
"input://.tmp/archive-viewer/SESSION_ID/new-file.txt",
"This file was added via the API!",
function (err) { console.log(err || "File added to archive"); }
);
// Refresh the view to see the new file
socket.fire("webapp::instance::request", "fs:refresh");<button onclick="window._ws('avSocket')&&window.avSocket.fire('webapp::instance::request','fs:cwd',function(e,cwd){if(cwd){window.avSocket.fire('webapp::instance::request','fs:write',cwd+'api-test.txt','Created via Archive Viewer API at '+new Date().toISOString(),function(e2){if(!e2){window.avSocket.fire('webapp::instance::request','fs:refresh');alert('File api-test.txt added to archive!')}else alert('Error: '+e2)})}else alert('Navigate to archive first')})">Try: Add api-test.txt to Current Archive</button>
socket.fire("webapp::instance::request", "fs:read",
"input://.tmp/archive-viewer/SESSION_ID/some-file.txt",
function (err, content) { console.log(content); }
);<button onclick="window.ws('avSocket')&&window.avSocket.fire('webapp::instance::request','fs:cwd',function(e,cwd){if(cwd){window.avSocket.fire('webapp::instance::request','fs:ls',cwd,function(e2,files){if(files&&files.length){var first=files.find(function(f){return !f.metadata.isDirectory});if(first){window.avSocket.fire('webapp::instance::request','fs:read',first.filename,function(e3,content){alert('File: '+first.filename.replace(/^.*\//,'')+'\n\nContent:\n'+(content||'(binary)').substring(0,500))})}else alert('No files found')}else alert('Empty directory')})}})">Try: Read First File in Archive</button>
socket.fire("webapp::instance::request", "fs:ls", "input://.tmp/archive-viewer/SESSION_ID/",
function (err, files) {
files.forEach(function (f) {
console.log(f.metadata._isDirectory ? "[DIR]" : "[FILE]", f.filename);
});
}
);<button onclick="window.ws('avSocket')&&window.avSocket.fire('webapp::instance::request','fs:cwd',function(e,cwd){if(cwd){window.avSocket.fire('webapp::instance::request','fs:ls',cwd,function(e2,files){if(files){alert('Archive contents ('+files.length+' items):\n\n'+files.map(function(f){return (f.metadata.isDirectory?'[DIR] ':'[FILE] ')+f.filename.replace(/^.*\/(?=[^\/]+\/?$)/,'')}).join('\n'))}else alert('Error: '+(e2||'empty'))})}})">Try: List Archive Contents</button>
socket.fire("webapp::instance::request", "fs:mkdirp",
"input://.tmp/archive-viewer/SESSION_ID/new-folder/sub-folder/",
function (err) {
socket.fire("webapp::instance::request", "fs:refresh");
}
);<button onclick="window._ws('avSocket')&&window.avSocket.fire('webapp::instance::request','fs:cwd',function(e,cwd){if(cwd){window.avSocket.fire('webapp::instance::request','fs:mkdirp',cwd+'new-folder/sub-folder/',function(e2){if(!e2){window.avSocket.fire('webapp::instance::request','fs:refresh');alert('Created new-folder/sub-folder/')}else alert('Error: '+e2)})}})">Try: Create new-folder/sub-folder/</button>
The footer has a split Download button. The main button downloads in the current archive's format; the ▾ caret opens a dropdown with all writable formats.
| Format | Extension | Notes |
|---|---|---|
| ZIP | .zip | DEFLATE compressed |
| TAR | .tar | Uncompressed POSIX ustar |
| TAR.GZ | .tar.gz | TAR + GZIP |
| GZIP | .gz | Single-file format — uses only the first entry |
| 7Z | .7z | LZMA compressed |
project.zip → project.tar.gz when downloaded as TAR.GZ). A newly-created empty archive defaults to archive.<ext>.<!DOCTYPE html>
<html>
<head><title>Archive Editor Widget</title></head>
<body>
<div style="display:flex;gap:8px;padding:10px;flex-wrap:wrap;">
<button id="add-file">Add README.md</button>
<button id="list">List Contents</button>
</div>
<iframe id="viewer" src="https://sgapps.io/online/webapp/archive-viewer"
width="100%" height="600" frameborder="0"></iframe>
<pre id="output" style="background:#f6f8fa;padding:10px;"></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;
socket.fire("webapp::instance::embed-mode", true);
});
document.getElementById('add-file').onclick = function () {
if (!ready) return;
// Get the current archive extraction path
socket.fire("webapp::instance::request", "fs:cwd", function (err, cwd) {
// Write a file into the archive
socket.fire("webapp::instance::request", "fs:write",
cwd + "README.md",
"# Archive Contents\n\nThis file was added programmatically.",
function (err) {
socket.fire("webapp::instance::request", "fs:refresh");
document.getElementById('output').textContent = err || "README.md added!";
}
);
});
};
document.getElementById('list').onclick = function () {
if (!ready) return;
socket.fire("webapp::instance::request", "fs:cwd", function (err, cwd) {
socket.fire("webapp::instance::request", "fs:ls", cwd, function (err, files) {
document.getElementById('output').textContent = files.map(function (f) {
return (f.metadata._isDirectory ? "[DIR] " : "[FILE] ") +
f.filename.replace(/^.*\/(?=[^\/]+\/?$)/, '');
}).join("\n");
});
});
};
</script>
</body>
</html>