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

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.

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