Skip to content

Commit e862ed9

Browse files
authored
release(v3.0.1): archive create/extract upgrades (7z + RAR via unar) + login focus fix (closes #82)
- add 7z archive format option for multi-file downloads (worker + download streaming) - expand extraction to support ZIP + 7z formats via 7z, with RAR preferring unar when available - harden archive extraction against traversal, symlinks, zip-bombs, and empty/escaped outputs - improve archive job robustness (stale job cleanup, clearer queued/worker errors, correct MIME/filenames) - UI: archive format selector + name normalization, better “Extract Archive” handling, i18n updates - fix login screen focus (auto-focus username when login prompt shows) Closes #82
1 parent 1ee8306 commit e862ed9

19 files changed

Lines changed: 1517 additions & 323 deletions

CHANGELOG.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,70 @@
11
# Changelog
22

3-
## Changes 1/11/2025 (V3.0.0)
3+
## Changes 01/14/2026 (v3.0.1 Archive update + login focus)
4+
5+
`release(v3.0.1): archive create/extract upgrades (7z + RAR via unar) + login focus fix (closes #82)`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.0.1): archive create/extract upgrades (7z + RAR via unar) + login focus fix (closes #82)
11+
12+
- add 7z archive format option for multi-file downloads (worker + download streaming)
13+
- expand extraction to support ZIP + 7z formats via 7z, with RAR preferring unar when available
14+
- harden archive extraction against traversal, symlinks, zip-bombs, and empty/escaped outputs
15+
- improve archive job robustness (stale job cleanup, clearer queued/worker errors, correct MIME/filenames)
16+
- UI: archive format selector + name normalization, better “Extract Archive” handling, i18n updates
17+
- fix login screen focus (auto-focus username when login prompt shows)
18+
19+
Closes #82
20+
```
21+
22+
**Added**
23+
24+
- **Archive download format selector (ZIP / 7z)** in the “Download Selected Files as Archive” modal.
25+
- **7z archive creation** support in the background worker (`zip_worker.php`) using `7zz/7z`.
26+
- **RAR extraction prefers `unar`** when available (FOSS-friendly); falls back to `7z` when needed.
27+
- **Archive detection helper** `isArchiveFileName()` supporting:
28+
- `.zip`, `.7z`, `.tar.*`, `.gz`, `.bz2`, `.xz`, `.rar`
29+
- RAR split parts like `.r01`, `.r02`, etc.
30+
31+
**Changed**
32+
33+
- **“ZIP” language → “Archive” language** across UI, admin notes, and translations.
34+
- **Archive job enqueue + download endpoint** now supports a `format` field (`zip` or `7z`):
35+
- download streaming sets correct extension + MIME type (`application/zip` or `application/x-7z-compressed`)
36+
- filename normalization strips any existing `.zip/.7z` and applies the chosen extension
37+
- **Archive extraction** is no longer ZIP-only:
38+
- ZIP still uses `ZipArchive`
39+
- non-ZIP formats use `7z` listing (`7z l -slt`) + extraction of an allow-listed set
40+
- RAR parts like `.r01` map to their base `.rar` / `.part1.rar` automatically
41+
- **Archive queue robustness**
42+
- stale queued/working jobs are cleaned up (PID checks + cmdline sanity where available)
43+
- queued jobs that never start can surface a clearer error message (“worker did not start…”)
44+
45+
**Fixed**
46+
47+
- **Login UX:** auto-focus username field when the login prompt appears (reduces “why can’t I type?” friction).
48+
- **Extract action visibility:** Extract button/menu now appears for supported archive formats (not just `.zip`).
49+
- **Better extraction feedback:** extraction API returns optional `warning` text; UI shows success + warning separately when partial issues occur.
50+
51+
**Security / Hardening**
52+
53+
- **Archive extraction safety controls**:
54+
- blocks absolute paths / traversal (`../`) and unsupported folder names
55+
- skips dotfiles (configurable) instead of extracting hidden entries by default
56+
- detects and skips symlinks and removes any symlinks created during extraction
57+
- zip-bomb limits: max uncompressed bytes + max files (configurable)
58+
- prunes empty outputs that indicate partial/broken extraction and removes any files that escape the extraction root
59+
60+
**Docker**
61+
62+
- Image now installs **7zip + unar** so archive create/extract works out-of-the-box with FOSS tooling.
63+
- Ubuntu repo components are restricted to **`main universe`** (avoids non-free repos by default).
64+
65+
---
66+
67+
## Changes 1/11/2026 (V3.0.0)
468

569
`release(v3.0.0): storage adapter seam + source-aware core (Sources-ready)`
670

@@ -131,7 +195,7 @@ FileRise v3.0.0 is a major internal milestone: a new storage adapter seam + sour
131195

132196
---
133197

134-
## Changes 1/2/2025 (v2.13.1)
198+
## Changes 1/2/2026 (v2.13.1)
135199

136200
`release(v2.13.1): harden Docker startup perms + explicit inline MIME mapping (see #79)`
137201

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ ENV DEBIAN_FRONTEND=noninteractive \
3333
PUID=99 PGID=100
3434

3535
# Install Apache, PHP, and required extensions
36-
RUN apt-get update && \
36+
RUN if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \
37+
sed -i 's/^Components: .*/Components: main universe/' /etc/apt/sources.list.d/ubuntu.sources; \
38+
fi && \
39+
apt-get update && \
3740
apt-get upgrade -y && \
3841
apt-get install -y --no-install-recommends \
3942
apache2 \
4043
php php-json php-curl php-zip php-mbstring php-gd php-xml \
4144
ca-certificates curl git openssl \
4245
smbclient \
46+
7zip \
47+
unar \
4348
clamav clamav-freshclam \
4449
&& apt-get clean && rm -rf /var/lib/apt/lists/*
4550

public/api/file/downloadZip.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
/**
66
* @OA\Post(
77
* path="/api/file/downloadZip.php",
8-
* summary="Download multiple files as a ZIP",
9-
* description="Requires view access (or own-only with ownership). May be gated by account flag.",
8+
* summary="Queue an archive download",
9+
* description="Queues a background archive build. Requires view access (or own-only with ownership). May be gated by account flag.",
1010
* operationId="downloadZip",
1111
* tags={"Files"},
1212
* security={{"cookieAuth": {}}},
@@ -16,18 +16,19 @@
1616
* @OA\JsonContent(
1717
* required={"folder","files"},
1818
* @OA\Property(property="folder", type="string", example="root"),
19-
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"a.jpg","b.png"})
19+
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"a.jpg","b.png"}),
20+
* @OA\Property(property="format", type="string", example="zip", enum={"zip","7z"}, description="Archive format")
2021
* )
2122
* ),
2223
* @OA\Response(
2324
* response=200,
24-
* description="ZIP archive",
25-
* content={
26-
* "application/zip": @OA\MediaType(
27-
* mediaType="application/zip",
28-
* @OA\Schema(type="string", format="binary")
29-
* )
30-
* }
25+
* description="Archive job queued",
26+
* @OA\JsonContent(
27+
* @OA\Property(property="ok", type="boolean", example=true),
28+
* @OA\Property(property="token", type="string"),
29+
* @OA\Property(property="statusUrl", type="string"),
30+
* @OA\Property(property="downloadUrl", type="string")
31+
* )
3132
* ),
3233
* @OA\Response(response=400, description="Invalid input"),
3334
* @OA\Response(response=401, description="Unauthorized"),
@@ -40,4 +41,4 @@
4041
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
4142

4243
$fileController = new FileController();
43-
$fileController->downloadZip();
44+
$fileController->downloadZip();

public/api/file/downloadZipFile.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
/**
55
* @OA\Get(
66
* path="/api/file/downloadZipFile.php",
7-
* summary="Download a finished ZIP by token",
8-
* description="Streams the zip once; token is one-shot.",
7+
* summary="Download a finished archive by token",
8+
* description="Streams the archive once; token is one-shot.",
99
* operationId="downloadZipFile",
1010
* tags={"Files"},
1111
* security={{"cookieAuth": {}}},
1212
* @OA\Parameter(name="k", in="query", required=true, @OA\Schema(type="string"), description="Job token"),
1313
* @OA\Parameter(name="name", in="query", required=false, @OA\Schema(type="string"), description="Suggested filename"),
14-
* @OA\Response(response=200, description="ZIP stream"),
14+
* @OA\Response(response=200, description="Archive stream"),
1515
* @OA\Response(response=401, description="Unauthorized"),
1616
* @OA\Response(response=404, description="Not found")
1717
* )
@@ -21,4 +21,4 @@
2121
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
2222

2323
$controller = new FileController();
24-
$controller->downloadZipFile();
24+
$controller->downloadZipFile();

public/api/file/extractZip.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
/**
55
* @OA\Post(
66
* path="/api/file/extractZip.php",
7-
* summary="Extract ZIP file(s) into a folder",
8-
* description="Requires write access on the target folder.",
7+
* summary="Extract archive file(s) into a folder",
8+
* description="Supports ZIP/7Z and RAR extraction via server tools. Requires write access on the target folder.",
99
* operationId="extractZip",
1010
* tags={"Files"},
1111
* security={{"cookieAuth": {}}},
@@ -15,7 +15,7 @@
1515
* @OA\JsonContent(
1616
* required={"folder","files"},
1717
* @OA\Property(property="folder", type="string", example="root"),
18-
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"archive.zip"})
18+
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"archive.zip","archive.7z"})
1919
* )
2020
* ),
2121
* @OA\Response(response=200, description="Extraction result (model-defined)"),
@@ -30,4 +30,4 @@
3030
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
3131

3232
$fileController = new FileController();
33-
$fileController->extractZip();
33+
$fileController->extractZip();

public/api/file/zipStatus.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/**
55
* @OA\Get(
66
* path="/api/file/zipStatus.php",
7-
* summary="Check status of a background ZIP build",
7+
* summary="Check status of a background archive build",
88
* description="Returns status for the authenticated user's token.",
99
* operationId="zipStatus",
1010
* tags={"Files"},
@@ -20,4 +20,4 @@
2020
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
2121

2222
$controller = new FileController();
23-
$controller->zipStatus();
23+
$controller->zipStatus();

public/index.html

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ <h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
326326
</div>
327327
<div class="action-separator secondary-separator" aria-hidden="true"></div>
328328
<div class="action-group secondary-actions">
329-
<button id="downloadZipBtn" class="btn action-btn icon-only" disabled title="Download selected" data-i18n-title="download">
329+
<button id="downloadZipBtn" class="btn action-btn icon-only" disabled title="Download archive" data-i18n-title="download_zip">
330330
<span class="material-icons" aria-hidden="true">file_download</span>
331331
</button>
332332
<button id="copySelectedBtn" class="btn action-btn icon-only" disabled title="Copy" data-i18n-title="copy_files">
@@ -344,7 +344,7 @@ <h2 id="fileListTitle" data-i18n-key="file_list_title">Files in (Root)</h2>
344344
<button id="deleteSelectedBtn" class="btn action-btn icon-only" disabled title="Delete" data-i18n-title="delete_files">
345345
<span class="material-icons" aria-hidden="true">delete</span>
346346
</button>
347-
<button id="extractZipBtn" class="btn action-btn icon-only" style="display: none;" disabled title="Extract zip" data-i18n-title="extract_zip_button">
347+
<button id="extractZipBtn" class="btn action-btn icon-only" style="display: none;" disabled title="Extract archive" data-i18n-title="extract_zip_button">
348348
<span class="material-icons" aria-hidden="true">unarchive</span>
349349
</button>
350350
<button id="toolbarMenuBtn" class="btn action-btn icon-only" title="More actions">
@@ -442,8 +442,13 @@ <h4 data-i18n-key="create_new_file">Create New File</h4>
442442
</div>
443443
<div id="downloadZipModal" class="modal" style="display:none;">
444444
<div class="modal-content">
445-
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
446-
<p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
445+
<h4 data-i18n-key="download_zip_title">Download Selected Files as Archive</h4>
446+
<label for="archiveFormatSelect" data-i18n-key="download_archive_format" style="display:block; margin-top:10px;">Archive format</label>
447+
<select id="archiveFormatSelect" class="form-control">
448+
<option value="zip">ZIP (.zip)</option>
449+
<option value="7z">7-Zip (.7z)</option>
450+
</select>
451+
<p data-i18n-key="download_zip_prompt" style="margin-top:10px;">Enter a name for the archive file:</p>
447452
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder"
448453
placeholder="files.zip" />
449454
<div class="modal-footer" style="margin-top:15px; text-align:right;">
@@ -579,7 +584,7 @@ <h3 data-i18n-key="create_new_user_title">Create New User</h3>
579584
data-action="download_zip"
580585
data-when="any">
581586
<i class="material-icons">archive</i>
582-
<span>Download as ZIP</span>
587+
<span>Download Archive</span>
583588
</button>
584589

585590
<!-- NEW: multi-download without ZIP -->
@@ -594,7 +599,7 @@ <h3 data-i18n-key="create_new_user_title">Create New User</h3>
594599
data-action="extract_zip"
595600
data-when="zip">
596601
<i class="material-icons">unarchive</i>
597-
<span>Extract ZIP</span>
602+
<span>Extract Archive</span>
598603
</button>
599604

600605
<div class="sep" data-when="any"></div>

public/js/adminFolderAccess.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,9 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
374374
${group(
375375
tf('write_full', 'Write/Modify'),
376376
`
377-
${toggle('write', tf('write_full', 'Write (file ops)'), writeMetaChecked, false, tf('write_help', 'File-level: upload, edit, rename, copy, delete, extract ZIPs (no folder creation).'))}
377+
${toggle('write', tf('write_full', 'Write (file ops)'), writeMetaChecked, false, tf('write_help', 'File-level: upload, edit, rename, copy, delete, extract archives (no folder creation).'))}
378378
${toggle('edit', tf('edit', 'Edit File'), g.edit, false, tf('edit_help', 'Edit file contents'))}
379-
${toggle('extract', tf('extract', 'Extract ZIP'), g.extract, false, tf('extract_help', 'Extract ZIP archives'))}
379+
${toggle('extract', tf('extract', 'Extract Archive'), g.extract, false, tf('extract_help', 'Extract archive files'))}
380380
${toggle('rename', tf('rename', 'Rename File'), g.rename, false, tf('rename_help', 'Rename a file'))}
381381
${toggle('copy', tf('copy', 'Copy File'), g.copy, false, tf('copy_help', 'Copy a file'))}
382382
`,

public/js/adminPanel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ function renderAdminEncryptionSection({ config, dark }) {
679679
</div>
680680
681681
<div class="small text-muted" style="margin-top:8px;">
682-
${tf("encryption_v1_note", "Admin notes:<ul style=\"margin:6px 0 0 18px; padding:0;\"><li>Master key can be set via <code>FR_ENCRYPTION_MASTER_KEY</code> (env overrides the key file) or via <code>META_DIR/encryption_master.key</code> (32 raw bytes).</li><li>Encrypted folders are recursive; shares, shared-folder uploads, WebDAV, and ZIP create/extract are blocked under encrypted folders.</li><li>Video/audio previews are disabled (no HTTP Range) but users can still download files normally.</li></ul>")}
682+
${tf("encryption_v1_note", "Admin notes:<ul style=\"margin:6px 0 0 18px; padding:0;\"><li>Master key can be set via <code>FR_ENCRYPTION_MASTER_KEY</code> (env overrides the key file) or via <code>META_DIR/encryption_master.key</code> (32 raw bytes).</li><li>Encrypted folders are recursive; shares, shared-folder uploads, WebDAV, and archive create/extract are blocked under encrypted folders.</li><li>Video/audio previews are disabled (no HTTP Range) but users can still download files normally.</li></ul>")}
683683
</div>
684684
</div>
685685
`;

public/js/domUtils.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22
import { t } from './i18n.js?v={{APP_QVER}}';
33
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
44

5+
const ARCHIVE_EXTS = [
6+
".zip",
7+
".7z",
8+
".tar",
9+
".tar.gz",
10+
".tgz",
11+
".tar.bz2",
12+
".tbz2",
13+
".tar.xz",
14+
".txz",
15+
".gz",
16+
".bz2",
17+
".xz",
18+
".rar"
19+
];
20+
const RAR_PART_RE = /\.r\d{2}$/i;
21+
22+
export function isArchiveFileName(name) {
23+
const lower = String(name || "").toLowerCase();
24+
if (!lower) return false;
25+
if (RAR_PART_RE.test(lower)) return true;
26+
return ARCHIVE_EXTS.some(ext => lower.endsWith(ext));
27+
}
28+
529
// Basic DOM Helpers
630
export function toggleVisibility(elementId, shouldShow) {
731
const element = document.getElementById(elementId);
@@ -60,7 +84,7 @@ export function updateFileActionButtons() {
6084
const anySelected = selectedCheckboxes.length > 0;
6185
const anyFolderSelected = selectedFolders.length > 0;
6286
const anyZip = Array.from(selectedCheckboxes)
63-
.some(cb => cb.value.toLowerCase().endsWith(".zip"));
87+
.some(cb => isArchiveFileName(cb.value));
6488
const singleSelected = selectedCheckboxes.length === 1;
6589
const currentFolderCaps = window.currentFolderCaps || null;
6690
const selectedFolderCaps = window.selectedFolderCaps || null;
@@ -158,7 +182,7 @@ export function updateFileActionButtons() {
158182
if (shareBtn) shareBtn.style.display = (showFileActions && !inEncryptedFolder) ? "" : "none";
159183
if (createBtn) createBtn.style.display = "";
160184

161-
// Extract ZIP still appears only when a .zip is selected (and file mode)
185+
// Extract archive appears only when a supported archive is selected (and file mode)
162186
if (extractZipBtn) extractZipBtn.style.display = (showFileActions && anyZip && !inEncryptedFolder) ? "" : "none";
163187

164188
// Finally disable the ones that are shown but shouldn’t be clickable

0 commit comments

Comments
 (0)