Archive Viewer

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.

Live Demo

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

Embed

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

Open a Remote Archive

var archiveUrl = "https://example.com/project.zip";
var src = "https://sgapps.io/online/webapp/archive-viewer/url/" + btoa(archiveUrl);

Open an Archive from a Blob (WebSocket)

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:

  1. fs:write the bytes to a path under input:// (session-scoped in-memory FS).
  2. 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 an ArrayBuffer for binary (archive bytes, images, any Blob/File → blob.arrayBuffer()), or a plain string for text. Blob/File instances cannot cross the WindowSocket bridge directly — they would arrive as empty &#x7B;&#x7D; on the webapp side. The receiving handler calls new 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:

Toggle Auto-save (WebSocket)

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>

Archive Lifecycle Events

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

Catalogue

Event Payload Fires on
archive:status (text, type) where type ∈ &#x7B;&#x22;info&#x22;,&#x22;success&#x22;,&#x22;error&#x22;&#x7D; 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 ∈ &#x7B;&#x22;api&#x22;,&#x22;ui&#x22;,&#x22;launch-arg&#x22;&#x7D; Auto-save flag flipped from any channel (see above)

Why both archive:save:done and afterWrite?

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>

Reverse Event — Auto-save State Changed

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 &#x22;api&#x22;, &#x22;ui&#x22;,or &#x22;launch-arg&#x22;.

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>

How It Works

  1. The archive is downloaded and extracted to browser-only memory (input:// protocol)
  2. The full File Manager UI lets you browse, view, edit, add, and delete files
  3. Text files open in the Code Editor, images in the Photo Editor
  4. Click Save Archive to rebuild the archive, or enable Auto-save
  5. The rebuilt archive is written back to the original path
  6. Click Download to save the archive to your computer — use the ▾ caret to pick a different format (ZIP, TAR, TAR.GZ, GZIP, 7Z)

Supported Formats

Format Read Write
ZIP Yes Yes
TAR Yes Yes
TAR.GZ Yes Yes
7Z Yes Yes
RAR Yes No (proprietary)
GZIP Yes Yes

Format is auto-detected when opening. When creating, you choose the format.


Filesystem Events

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.

Detect when the archive is saved

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.

Add Files to the Archive via API

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

Read a File from the Archive

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>

List Archive Contents

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>

Create Directories in the Archive

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>


Features

Download & Format Conversion

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

RAR archives can be opened but not written (proprietary format); in that case, the primary Download button falls back to ZIP. Use the dropdown to convert a RAR into any supported write format.
The filename is the original archive name with the new extension (e.g. project.zip → project.tar.gz when downloaded as TAR.GZ). A newly-created empty archive defaults to archive.&#x3C;ext&#x3E;.

Complete Example

<!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>
sgapps.io