# 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

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

### Open a Remote Archive

```js
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](file-manager.md). 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

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

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

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

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

- Full File Manager interface (sidebar, breadcrumbs, context menus, view modes)
- Edit text files in-place, view images, play media
- Upload new files into the archive via drag-and-drop or context menu
- Delete files from the archive
- Status bar shows archive size and unpacked size
- Auto-save mode with 2-second debounce
- **Download** the archive to your computer, or use the **Download as...** dropdown to convert between formats (ZIP / TAR / TAR.GZ / GZIP / 7Z). The download always reflects the current in-memory state, so unsaved edits are included
- All filesystem events from File Manager available for programmatic control

## Download &amp; 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.<ext>`.

## Complete Example

```html
<!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>
```
