mirror of
https://github.com/error311/FileRise.git
synced 2026-05-16 00:40:50 -05:00
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
This commit is contained in:
+66
-2
@@ -1,6 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 1/11/2025 (V3.0.0)
|
||||
## Changes 01/14/2026 (v3.0.1 Archive update + login focus)
|
||||
|
||||
`release(v3.0.1): archive create/extract upgrades (7z + RAR via unar) + login focus fix (closes #82)`
|
||||
|
||||
**Commit message**
|
||||
|
||||
```text
|
||||
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
|
||||
```
|
||||
|
||||
**Added**
|
||||
|
||||
- **Archive download format selector (ZIP / 7z)** in the “Download Selected Files as Archive” modal.
|
||||
- **7z archive creation** support in the background worker (`zip_worker.php`) using `7zz/7z`.
|
||||
- **RAR extraction prefers `unar`** when available (FOSS-friendly); falls back to `7z` when needed.
|
||||
- **Archive detection helper** `isArchiveFileName()` supporting:
|
||||
- `.zip`, `.7z`, `.tar.*`, `.gz`, `.bz2`, `.xz`, `.rar`
|
||||
- RAR split parts like `.r01`, `.r02`, etc.
|
||||
|
||||
**Changed**
|
||||
|
||||
- **“ZIP” language → “Archive” language** across UI, admin notes, and translations.
|
||||
- **Archive job enqueue + download endpoint** now supports a `format` field (`zip` or `7z`):
|
||||
- download streaming sets correct extension + MIME type (`application/zip` or `application/x-7z-compressed`)
|
||||
- filename normalization strips any existing `.zip/.7z` and applies the chosen extension
|
||||
- **Archive extraction** is no longer ZIP-only:
|
||||
- ZIP still uses `ZipArchive`
|
||||
- non-ZIP formats use `7z` listing (`7z l -slt`) + extraction of an allow-listed set
|
||||
- RAR parts like `.r01` map to their base `.rar` / `.part1.rar` automatically
|
||||
- **Archive queue robustness**
|
||||
- stale queued/working jobs are cleaned up (PID checks + cmdline sanity where available)
|
||||
- queued jobs that never start can surface a clearer error message (“worker did not start…”)
|
||||
|
||||
**Fixed**
|
||||
|
||||
- **Login UX:** auto-focus username field when the login prompt appears (reduces “why can’t I type?” friction).
|
||||
- **Extract action visibility:** Extract button/menu now appears for supported archive formats (not just `.zip`).
|
||||
- **Better extraction feedback:** extraction API returns optional `warning` text; UI shows success + warning separately when partial issues occur.
|
||||
|
||||
**Security / Hardening**
|
||||
|
||||
- **Archive extraction safety controls**:
|
||||
- blocks absolute paths / traversal (`../`) and unsupported folder names
|
||||
- skips dotfiles (configurable) instead of extracting hidden entries by default
|
||||
- detects and skips symlinks and removes any symlinks created during extraction
|
||||
- zip-bomb limits: max uncompressed bytes + max files (configurable)
|
||||
- prunes empty outputs that indicate partial/broken extraction and removes any files that escape the extraction root
|
||||
|
||||
**Docker**
|
||||
|
||||
- Image now installs **7zip + unar** so archive create/extract works out-of-the-box with FOSS tooling.
|
||||
- Ubuntu repo components are restricted to **`main universe`** (avoids non-free repos by default).
|
||||
|
||||
---
|
||||
|
||||
## Changes 1/11/2026 (V3.0.0)
|
||||
|
||||
`release(v3.0.0): storage adapter seam + source-aware core (Sources-ready)`
|
||||
|
||||
@@ -131,7 +195,7 @@ FileRise v3.0.0 is a major internal milestone: a new storage adapter seam + sour
|
||||
|
||||
---
|
||||
|
||||
## Changes 1/2/2025 (v2.13.1)
|
||||
## Changes 1/2/2026 (v2.13.1)
|
||||
|
||||
`release(v2.13.1): harden Docker startup perms + explicit inline MIME mapping (see #79)`
|
||||
|
||||
|
||||
+6
-1
@@ -33,13 +33,18 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PUID=99 PGID=100
|
||||
|
||||
# Install Apache, PHP, and required extensions
|
||||
RUN apt-get update && \
|
||||
RUN if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \
|
||||
sed -i 's/^Components: .*/Components: main universe/' /etc/apt/sources.list.d/ubuntu.sources; \
|
||||
fi && \
|
||||
apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
apache2 \
|
||||
php php-json php-curl php-zip php-mbstring php-gd php-xml \
|
||||
ca-certificates curl git openssl \
|
||||
smbclient \
|
||||
7zip \
|
||||
unar \
|
||||
clamav clamav-freshclam \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/downloadZip.php",
|
||||
* summary="Download multiple files as a ZIP",
|
||||
* description="Requires view access (or own-only with ownership). May be gated by account flag.",
|
||||
* summary="Queue an archive download",
|
||||
* description="Queues a background archive build. Requires view access (or own-only with ownership). May be gated by account flag.",
|
||||
* operationId="downloadZip",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
@@ -16,18 +16,19 @@
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","files"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"a.jpg","b.png"})
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"a.jpg","b.png"}),
|
||||
* @OA\Property(property="format", type="string", example="zip", enum={"zip","7z"}, description="Archive format")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="ZIP archive",
|
||||
* content={
|
||||
* "application/zip": @OA\MediaType(
|
||||
* mediaType="application/zip",
|
||||
* @OA\Schema(type="string", format="binary")
|
||||
* )
|
||||
* }
|
||||
* description="Archive job queued",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="ok", type="boolean", example=true),
|
||||
* @OA\Property(property="token", type="string"),
|
||||
* @OA\Property(property="statusUrl", type="string"),
|
||||
* @OA\Property(property="downloadUrl", type="string")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=400, description="Invalid input"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
@@ -40,4 +41,4 @@ require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->downloadZip();
|
||||
$fileController->downloadZip();
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/downloadZipFile.php",
|
||||
* summary="Download a finished ZIP by token",
|
||||
* description="Streams the zip once; token is one-shot.",
|
||||
* summary="Download a finished archive by token",
|
||||
* description="Streams the archive once; token is one-shot.",
|
||||
* operationId="downloadZipFile",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
* @OA\Parameter(name="k", in="query", required=true, @OA\Schema(type="string"), description="Job token"),
|
||||
* @OA\Parameter(name="name", in="query", required=false, @OA\Schema(type="string"), description="Suggested filename"),
|
||||
* @OA\Response(response=200, description="ZIP stream"),
|
||||
* @OA\Response(response=200, description="Archive stream"),
|
||||
* @OA\Response(response=401, description="Unauthorized"),
|
||||
* @OA\Response(response=404, description="Not found")
|
||||
* )
|
||||
@@ -21,4 +21,4 @@ require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$controller = new FileController();
|
||||
$controller->downloadZipFile();
|
||||
$controller->downloadZipFile();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/file/extractZip.php",
|
||||
* summary="Extract ZIP file(s) into a folder",
|
||||
* description="Requires write access on the target folder.",
|
||||
* summary="Extract archive file(s) into a folder",
|
||||
* description="Supports ZIP/7Z and RAR extraction via server tools. Requires write access on the target folder.",
|
||||
* operationId="extractZip",
|
||||
* tags={"Files"},
|
||||
* security={{"cookieAuth": {}}},
|
||||
@@ -15,7 +15,7 @@
|
||||
* @OA\JsonContent(
|
||||
* required={"folder","files"},
|
||||
* @OA\Property(property="folder", type="string", example="root"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"archive.zip"})
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="string"), example={"archive.zip","archive.7z"})
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Extraction result (model-defined)"),
|
||||
@@ -30,4 +30,4 @@ require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$fileController = new FileController();
|
||||
$fileController->extractZip();
|
||||
$fileController->extractZip();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/file/zipStatus.php",
|
||||
* summary="Check status of a background ZIP build",
|
||||
* summary="Check status of a background archive build",
|
||||
* description="Returns status for the authenticated user's token.",
|
||||
* operationId="zipStatus",
|
||||
* tags={"Files"},
|
||||
@@ -20,4 +20,4 @@ require_once __DIR__ . '/../../../config/config.php';
|
||||
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||
|
||||
$controller = new FileController();
|
||||
$controller->zipStatus();
|
||||
$controller->zipStatus();
|
||||
|
||||
+11
-6
@@ -326,7 +326,7 @@
|
||||
</div>
|
||||
<div class="action-separator secondary-separator" aria-hidden="true"></div>
|
||||
<div class="action-group secondary-actions">
|
||||
<button id="downloadZipBtn" class="btn action-btn icon-only" disabled title="Download selected" data-i18n-title="download">
|
||||
<button id="downloadZipBtn" class="btn action-btn icon-only" disabled title="Download archive" data-i18n-title="download_zip">
|
||||
<span class="material-icons" aria-hidden="true">file_download</span>
|
||||
</button>
|
||||
<button id="copySelectedBtn" class="btn action-btn icon-only" disabled title="Copy" data-i18n-title="copy_files">
|
||||
@@ -344,7 +344,7 @@
|
||||
<button id="deleteSelectedBtn" class="btn action-btn icon-only" disabled title="Delete" data-i18n-title="delete_files">
|
||||
<span class="material-icons" aria-hidden="true">delete</span>
|
||||
</button>
|
||||
<button id="extractZipBtn" class="btn action-btn icon-only" style="display: none;" disabled title="Extract zip" data-i18n-title="extract_zip_button">
|
||||
<button id="extractZipBtn" class="btn action-btn icon-only" style="display: none;" disabled title="Extract archive" data-i18n-title="extract_zip_button">
|
||||
<span class="material-icons" aria-hidden="true">unarchive</span>
|
||||
</button>
|
||||
<button id="toolbarMenuBtn" class="btn action-btn icon-only" title="More actions">
|
||||
@@ -442,8 +442,13 @@
|
||||
</div>
|
||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
||||
<p data-i18n-key="download_zip_prompt">Enter a name for the zip file:</p>
|
||||
<h4 data-i18n-key="download_zip_title">Download Selected Files as Archive</h4>
|
||||
<label for="archiveFormatSelect" data-i18n-key="download_archive_format" style="display:block; margin-top:10px;">Archive format</label>
|
||||
<select id="archiveFormatSelect" class="form-control">
|
||||
<option value="zip">ZIP (.zip)</option>
|
||||
<option value="7z">7-Zip (.7z)</option>
|
||||
</select>
|
||||
<p data-i18n-key="download_zip_prompt" style="margin-top:10px;">Enter a name for the archive file:</p>
|
||||
<input type="text" id="zipFileNameInput" class="form-control" data-i18n-placeholder="zip_placeholder"
|
||||
placeholder="files.zip" />
|
||||
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
||||
@@ -579,7 +584,7 @@
|
||||
data-action="download_zip"
|
||||
data-when="any">
|
||||
<i class="material-icons">archive</i>
|
||||
<span>Download as ZIP</span>
|
||||
<span>Download Archive</span>
|
||||
</button>
|
||||
|
||||
<!-- NEW: multi-download without ZIP -->
|
||||
@@ -594,7 +599,7 @@
|
||||
data-action="extract_zip"
|
||||
data-when="zip">
|
||||
<i class="material-icons">unarchive</i>
|
||||
<span>Extract ZIP</span>
|
||||
<span>Extract Archive</span>
|
||||
</button>
|
||||
|
||||
<div class="sep" data-when="any"></div>
|
||||
|
||||
@@ -374,9 +374,9 @@ function renderFolderGrantsUI(principal, container, folders, grants) {
|
||||
${group(
|
||||
tf('write_full', 'Write/Modify'),
|
||||
`
|
||||
${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).'))}
|
||||
${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).'))}
|
||||
${toggle('edit', tf('edit', 'Edit File'), g.edit, false, tf('edit_help', 'Edit file contents'))}
|
||||
${toggle('extract', tf('extract', 'Extract ZIP'), g.extract, false, tf('extract_help', 'Extract ZIP archives'))}
|
||||
${toggle('extract', tf('extract', 'Extract Archive'), g.extract, false, tf('extract_help', 'Extract archive files'))}
|
||||
${toggle('rename', tf('rename', 'Rename File'), g.rename, false, tf('rename_help', 'Rename a file'))}
|
||||
${toggle('copy', tf('copy', 'Copy File'), g.copy, false, tf('copy_help', 'Copy a file'))}
|
||||
`,
|
||||
|
||||
@@ -679,7 +679,7 @@ function renderAdminEncryptionSection({ config, dark }) {
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" style="margin-top:8px;">
|
||||
${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>")}
|
||||
${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>")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+26
-2
@@ -2,6 +2,30 @@
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}';
|
||||
|
||||
const ARCHIVE_EXTS = [
|
||||
".zip",
|
||||
".7z",
|
||||
".tar",
|
||||
".tar.gz",
|
||||
".tgz",
|
||||
".tar.bz2",
|
||||
".tbz2",
|
||||
".tar.xz",
|
||||
".txz",
|
||||
".gz",
|
||||
".bz2",
|
||||
".xz",
|
||||
".rar"
|
||||
];
|
||||
const RAR_PART_RE = /\.r\d{2}$/i;
|
||||
|
||||
export function isArchiveFileName(name) {
|
||||
const lower = String(name || "").toLowerCase();
|
||||
if (!lower) return false;
|
||||
if (RAR_PART_RE.test(lower)) return true;
|
||||
return ARCHIVE_EXTS.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
// Basic DOM Helpers
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
const element = document.getElementById(elementId);
|
||||
@@ -60,7 +84,7 @@ export function updateFileActionButtons() {
|
||||
const anySelected = selectedCheckboxes.length > 0;
|
||||
const anyFolderSelected = selectedFolders.length > 0;
|
||||
const anyZip = Array.from(selectedCheckboxes)
|
||||
.some(cb => cb.value.toLowerCase().endsWith(".zip"));
|
||||
.some(cb => isArchiveFileName(cb.value));
|
||||
const singleSelected = selectedCheckboxes.length === 1;
|
||||
const currentFolderCaps = window.currentFolderCaps || null;
|
||||
const selectedFolderCaps = window.selectedFolderCaps || null;
|
||||
@@ -158,7 +182,7 @@ export function updateFileActionButtons() {
|
||||
if (shareBtn) shareBtn.style.display = (showFileActions && !inEncryptedFolder) ? "" : "none";
|
||||
if (createBtn) createBtn.style.display = "";
|
||||
|
||||
// Extract ZIP still appears only when a .zip is selected (and file mode)
|
||||
// Extract archive appears only when a supported archive is selected (and file mode)
|
||||
if (extractZipBtn) extractZipBtn.style.display = (showFileActions && anyZip && !inEncryptedFolder) ? "" : "none";
|
||||
|
||||
// Finally disable the ones that are shown but shouldn’t be clickable
|
||||
|
||||
+180
-102
@@ -1,5 +1,5 @@
|
||||
// fileActions.js
|
||||
import { showToast, attachEnterKeyListener, escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { showToast, attachEnterKeyListener, escapeHTML, isArchiveFileName } from './domUtils.js?v={{APP_QVER}}';
|
||||
import {
|
||||
loadFileList,
|
||||
formatFolderName,
|
||||
@@ -54,6 +54,54 @@ function getTransferTotalsForNames(names) {
|
||||
};
|
||||
}
|
||||
|
||||
const ARCHIVE_FORMATS = ["zip", "7z"];
|
||||
const ARCHIVE_NAME_SUFFIXES = ["zip", "7z", "rar"];
|
||||
const ARCHIVE_EXT_RE = /\.(zip|7z|rar)$/i;
|
||||
|
||||
function syncArchiveFormatSelect() {
|
||||
const select = document.getElementById("archiveFormatSelect");
|
||||
if (!select) return;
|
||||
const rarOption = select.querySelector('option[value="rar"]');
|
||||
if (rarOption) {
|
||||
rarOption.remove();
|
||||
}
|
||||
if (!ARCHIVE_FORMATS.includes(select.value)) {
|
||||
select.value = "zip";
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedArchiveFormat() {
|
||||
const select = document.getElementById("archiveFormatSelect");
|
||||
const value = select ? String(select.value || "").toLowerCase() : "zip";
|
||||
return ARCHIVE_FORMATS.includes(value) ? value : "zip";
|
||||
}
|
||||
|
||||
function normalizeArchiveName(raw, format) {
|
||||
let name = String(raw || "").trim();
|
||||
if (!name) return "";
|
||||
const ext = format === "7z" ? "7z" : format;
|
||||
const lower = name.toLowerCase();
|
||||
for (const suffix of ARCHIVE_NAME_SUFFIXES) {
|
||||
const token = "." + suffix;
|
||||
if (lower.endsWith(token)) {
|
||||
name = name.slice(0, -token.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name === "") name = "files";
|
||||
return name + "." + ext;
|
||||
}
|
||||
|
||||
function updateArchiveNamePlaceholder(format) {
|
||||
const input = document.getElementById("zipFileNameInput");
|
||||
if (!input) return;
|
||||
const ext = format === "7z" ? "7z" : format;
|
||||
input.placeholder = `files.${ext}`;
|
||||
if (input.value && ARCHIVE_EXT_RE.test(input.value)) {
|
||||
input.value = normalizeArchiveName(input.value, format);
|
||||
}
|
||||
}
|
||||
|
||||
let __copyMoveSourcesCache = null;
|
||||
|
||||
function getActivePaneKey() {
|
||||
@@ -359,7 +407,7 @@ export function handleDownloadMultiSelected(e) {
|
||||
const caps = window.currentFolderCaps || null;
|
||||
const inEncryptedFolder = !!(caps && caps.encryption && caps.encryption.encrypted);
|
||||
|
||||
// In encrypted folders, ZIP create is disabled. Allow plain downloads up to the limit only.
|
||||
// In encrypted folders, archive creation is disabled. Allow plain downloads up to the limit only.
|
||||
if (inEncryptedFolder) {
|
||||
if (files.length > limit) {
|
||||
showToast(`In encrypted folders, downloads are limited to ${limit} file(s) at a time.`, 'warning');
|
||||
@@ -369,7 +417,7 @@ export function handleDownloadMultiSelected(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal behavior: download individually up to the limit; ZIP for more than the limit.
|
||||
// Normal behavior: download individually up to the limit; archive for more than the limit.
|
||||
if (files.length > limit) {
|
||||
handleDownloadZipSelected(e || new Event("click"));
|
||||
return;
|
||||
@@ -394,7 +442,7 @@ export function handleDownloadZipSelected(e) {
|
||||
showToast(`In encrypted folders, downloads are limited to ${limit} file(s) at a time.`, 'warning');
|
||||
return;
|
||||
}
|
||||
// If we got here via an old/hidden ZIP action, fall back to plain download.
|
||||
// If we got here via an old/hidden archive action, fall back to plain download.
|
||||
downloadSelectedFilesIndividually(files);
|
||||
return;
|
||||
}
|
||||
@@ -406,6 +454,8 @@ export function handleDownloadZipSelected(e) {
|
||||
}
|
||||
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
|
||||
document.getElementById("downloadZipModal").style.display = "block";
|
||||
syncArchiveFormatSelect();
|
||||
updateArchiveNamePlaceholder(getSelectedArchiveFormat());
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("zipFileNameInput");
|
||||
input.focus();
|
||||
@@ -546,11 +596,11 @@ export function handleExtractZipSelected(e) {
|
||||
showToast("No files selected.", 'warning');
|
||||
return;
|
||||
}
|
||||
const zipFiles = Array.from(checkboxes)
|
||||
const archiveFiles = Array.from(checkboxes)
|
||||
.map(chk => chk.value)
|
||||
.filter(name => name.toLowerCase().endsWith(".zip"));
|
||||
if (!zipFiles.length) {
|
||||
showToast("No zip files selected.", 'warning');
|
||||
.filter(name => isArchiveFileName(name));
|
||||
if (!archiveFiles.length) {
|
||||
showToast("No archive files selected.", 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,27 +627,35 @@ export function handleExtractZipSelected(e) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || "root",
|
||||
files: zipFiles
|
||||
files: archiveFiles
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
modal.style.display = "none";
|
||||
const extracted = Array.isArray(data.extractedFiles) ? data.extractedFiles : [];
|
||||
const extractedMsg = extracted.length ? ("Extracted: " + extracted.join(", ")) : "Archive extracted.";
|
||||
const warning = (data && typeof data.warning === "string" && data.warning.trim()) ? data.warning.trim() : "";
|
||||
|
||||
if (data.success) {
|
||||
let msg = "Zip file(s) extracted successfully!";
|
||||
if (Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
msg = "Extracted: " + data.extractedFiles.join(", ");
|
||||
if (warning) {
|
||||
showToast(`${extractedMsg} Warning: ${warning}`, 'warning');
|
||||
} else {
|
||||
showToast(extractedMsg, 'success');
|
||||
}
|
||||
showToast(msg, 'success');
|
||||
loadFileList(window.currentFolder);
|
||||
} else if (extracted.length) {
|
||||
const warnMsg = warning || data.error || "Some files failed to extract.";
|
||||
showToast(`${extractedMsg} Warning: ${warnMsg}`, 'warning');
|
||||
} else {
|
||||
showToast("Error extracting zip: " + (data.error || "Unknown error"), 'error');
|
||||
const errMsg = warning || data.error || "Unknown error";
|
||||
showToast("Error extracting archive: " + errMsg, 'error');
|
||||
}
|
||||
loadFileList(window.currentFolder);
|
||||
})
|
||||
.catch(error => {
|
||||
modal.style.display = "none";
|
||||
console.error("Error extracting zip files:", error);
|
||||
showToast("Error extracting zip files.", 'error');
|
||||
console.error("Error extracting archive files:", error);
|
||||
showToast("Error extracting archive files.", 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -606,6 +664,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
const formatSelect = document.getElementById("archiveFormatSelect");
|
||||
const cancelCreate = document.getElementById('cancelCreateFile');
|
||||
|
||||
if (cancelCreate && !cancelCreate.__wiredCreateFile) {
|
||||
@@ -631,41 +690,51 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Confirm button kicks off the zip+download
|
||||
if (formatSelect) {
|
||||
syncArchiveFormatSelect();
|
||||
formatSelect.addEventListener("change", () => {
|
||||
updateArchiveNamePlaceholder(getSelectedArchiveFormat());
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Confirm button kicks off the archive+download
|
||||
if (confirmZipBtn) {
|
||||
confirmZipBtn.setAttribute("data-default", "");
|
||||
confirmZipBtn.addEventListener("click", async () => {
|
||||
// a) Validate ZIP filename
|
||||
let zipName = document.getElementById("zipFileNameInput").value.trim();
|
||||
if (!zipName) { showToast("Please enter a name for the zip file.", 'warning'); return; }
|
||||
if (!zipName.toLowerCase().endsWith(".zip")) zipName += ".zip";
|
||||
let archiveName = '';
|
||||
let ui = null;
|
||||
try {
|
||||
// a) Validate archive filename
|
||||
const format = getSelectedArchiveFormat();
|
||||
const rawName = document.getElementById("zipFileNameInput").value.trim();
|
||||
if (!rawName) { showToast("Please enter a name for the archive file.", 'warning'); return; }
|
||||
archiveName = normalizeArchiveName(rawName, format);
|
||||
|
||||
// b) Hide the name‐input modal, show the progress modal
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
// b) Hide the name‐input modal, show the progress modal
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
|
||||
// c) Title text (optional)
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
if (titleEl) titleEl.textContent = `Preparing ${zipName}…`;
|
||||
// c) Title text (optional)
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
if (titleEl) titleEl.textContent = `Preparing ${archiveName}…`;
|
||||
|
||||
// d) Queue the job
|
||||
const res = await fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ folder: window.currentFolder || "root", files: window.filesToDownload })
|
||||
});
|
||||
const jsr = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !jsr.ok) {
|
||||
const msg = (jsr && jsr.error) ? jsr.error : `Status ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
const token = jsr.token;
|
||||
const statusUrl = jsr.statusUrl;
|
||||
const downloadUrl = jsr.downloadUrl + "&name=" + encodeURIComponent(zipName);
|
||||
// d) Queue the job
|
||||
const res = await fetch("/api/file/downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||
body: JSON.stringify({ folder: window.currentFolder || "root", files: window.filesToDownload, format })
|
||||
});
|
||||
const jsr = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !jsr.ok) {
|
||||
const msg = (jsr && jsr.error) ? jsr.error : `Status ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
const statusUrl = jsr.statusUrl;
|
||||
const downloadUrl = jsr.downloadUrl + "&name=" + encodeURIComponent(archiveName);
|
||||
|
||||
// Ensure a progress UI exists in the modal
|
||||
function ensureZipProgressUI() {
|
||||
// Ensure a progress UI exists in the modal
|
||||
function ensureZipProgressUI() {
|
||||
const modalEl = document.getElementById("downloadProgressModal");
|
||||
if (!modalEl) {
|
||||
// really shouldn't happen, but fall back to body
|
||||
@@ -753,71 +822,80 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const ui = ensureZipProgressUI();
|
||||
const t0 = Date.now();
|
||||
ui = ensureZipProgressUI();
|
||||
const t0 = Date.now();
|
||||
|
||||
// e) Poll until ready
|
||||
while (true) {
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
const s = await fetch(`${statusUrl}&_=${Date.now()}`, {
|
||||
credentials: "include", cache: "no-store",
|
||||
}).then(r => r.json());
|
||||
// e) Poll until ready
|
||||
while (true) {
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
const s = await fetch(`${statusUrl}&_=${Date.now()}`, {
|
||||
credentials: "include", cache: "no-store",
|
||||
}).then(r => r.json());
|
||||
|
||||
if (s.error) throw new Error(s.error);
|
||||
if (ui.title) ui.title.textContent = `Preparing ${zipName}…`;
|
||||
if (s.error) throw new Error(s.error);
|
||||
if (ui.title) ui.title.textContent = `Preparing ${archiveName}…`;
|
||||
|
||||
// --- RENDER PROGRESS ---
|
||||
if (typeof s.pct === "number" && ui.bar && ui.text) {
|
||||
if ((s.phase !== 'finalizing') && (s.pct < 99)) {
|
||||
ui.hideSpinner && ui.hideSpinner();
|
||||
const filesDone = s.filesDone ?? 0;
|
||||
const filesTotal = s.filesTotal ?? 0;
|
||||
const bytesDone = s.bytesDone ?? 0;
|
||||
const bytesTotal = s.bytesTotal ?? 0;
|
||||
// --- RENDER PROGRESS ---
|
||||
if (s.status === "queued" && ui.text) {
|
||||
ui.text.textContent = "Queued… starting worker";
|
||||
} else if (typeof s.pct === "number" && ui.bar && ui.text) {
|
||||
if ((s.phase !== 'finalizing') && (s.pct < 99)) {
|
||||
ui.hideSpinner && ui.hideSpinner();
|
||||
const filesDone = s.filesDone ?? 0;
|
||||
const filesTotal = s.filesTotal ?? 0;
|
||||
const bytesDone = s.bytesDone ?? 0;
|
||||
const bytesTotal = s.bytesTotal ?? 0;
|
||||
|
||||
// Determinate 0–98% while enumerating
|
||||
const pct = Math.max(0, Math.min(98, s.pct | 0));
|
||||
if (!ui.bar.hasAttribute("value")) ui.bar.value = 0;
|
||||
ui.bar.value = pct;
|
||||
ui.text.textContent =
|
||||
`${pct}% — ${filesDone}/${filesTotal} files, ${humanBytes(bytesDone)} / ${humanBytes(bytesTotal)}`;
|
||||
} else {
|
||||
// FINALIZING: keep progress at 100% and show timer + selected totals
|
||||
if (!ui.bar.hasAttribute("value")) ui.bar.value = 100;
|
||||
ui.bar.value = 100; // lock at 100 during finalizing
|
||||
const since = s.finalizeAt ? Math.max(0, (Date.now() / 1000 | 0) - (s.finalizeAt | 0)) : 0;
|
||||
const selF = s.selectedFiles ?? s.filesTotal ?? 0;
|
||||
const selB = s.selectedBytes ?? s.bytesTotal ?? 0;
|
||||
ui.text.textContent = `Finalizing… ${mmss(since)} — ${selF} file${selF === 1 ? '' : 's'}, ~${humanBytes(selB)}`;
|
||||
// Determinate 0–98% while enumerating
|
||||
const pct = Math.max(0, Math.min(98, s.pct | 0));
|
||||
if (!ui.bar.hasAttribute("value")) ui.bar.value = 0;
|
||||
ui.bar.value = pct;
|
||||
ui.text.textContent =
|
||||
`${pct}% — ${filesDone}/${filesTotal} files, ${humanBytes(bytesDone)} / ${humanBytes(bytesTotal)}`;
|
||||
} else {
|
||||
// FINALIZING: keep progress at 100% and show timer + selected totals
|
||||
if (!ui.bar.hasAttribute("value")) ui.bar.value = 100;
|
||||
ui.bar.value = 100; // lock at 100 during finalizing
|
||||
const since = s.finalizeAt ? Math.max(0, (Date.now() / 1000 | 0) - (s.finalizeAt | 0)) : 0;
|
||||
const selF = s.selectedFiles ?? s.filesTotal ?? 0;
|
||||
const selB = s.selectedBytes ?? s.bytesTotal ?? 0;
|
||||
ui.text.textContent = `Finalizing… ${mmss(since)} — ${selF} file${selF === 1 ? '' : 's'}, ~${humanBytes(selB)}`;
|
||||
}
|
||||
} else if (ui.text) {
|
||||
ui.text.textContent = "Still preparing…";
|
||||
}
|
||||
} else if (ui.text) {
|
||||
ui.text.textContent = "Still preparing…";
|
||||
}
|
||||
// --- /RENDER ---
|
||||
// --- /RENDER ---
|
||||
|
||||
if (s.ready) {
|
||||
// Snap to 100 and close modal just before download
|
||||
if (ui.bar) { ui.bar.max = 100; ui.bar.value = 100; }
|
||||
progressModal.style.display = "none";
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
break;
|
||||
if (s.ready) {
|
||||
// Snap to 100 and close modal just before download
|
||||
if (ui.bar) { ui.bar.max = 100; ui.bar.value = 100; }
|
||||
progressModal.style.display = "none";
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
break;
|
||||
}
|
||||
if (Date.now() - t0 > 15 * 60 * 1000) throw new Error("Timed out preparing archive");
|
||||
}
|
||||
if (Date.now() - t0 > 15 * 60 * 1000) throw new Error("Timed out preparing ZIP");
|
||||
|
||||
// f) Trigger download
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
a.download = archiveName;
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
// g) Reset for next time
|
||||
if (ui.bar) ui.bar.value = 0;
|
||||
if (ui.text) ui.text.textContent = "";
|
||||
if (Array.isArray(window.filesToDownload)) window.filesToDownload = [];
|
||||
} catch (err) {
|
||||
const msg = (err && err.message) ? err.message : "Failed to prepare archive.";
|
||||
progressModal.style.display = "none";
|
||||
if (ui && ui.bar) ui.bar.value = 0;
|
||||
if (ui && ui.text) ui.text.textContent = "";
|
||||
showToast(msg, 'error');
|
||||
}
|
||||
|
||||
// f) Trigger download
|
||||
const a = document.createElement("a");
|
||||
a.href = downloadUrl;
|
||||
a.download = zipName;
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
// g) Reset for next time
|
||||
if (ui.bar) ui.bar.value = 0;
|
||||
if (ui.text) ui.text.textContent = "";
|
||||
if (Array.isArray(window.filesToDownload)) window.filesToDownload = [];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ function updateEncryptedFolderBanner(folder) {
|
||||
text.className = 'fr-enc-text';
|
||||
text.textContent =
|
||||
`This folder${rootLabel} is encrypted. ` +
|
||||
'Video/audio previews, WebDAV, ONLYOFFICE, and ZIP create/extract are disabled.';
|
||||
'Video/audio previews, WebDAV, ONLYOFFICE, and archive create/extract are disabled.';
|
||||
|
||||
el.append(pill, text);
|
||||
}
|
||||
@@ -7599,7 +7599,7 @@ export function downloadSelectedFilesIndividually(fileObjs) {
|
||||
const inEncrypted = !!(window.currentFolderCaps && window.currentFolderCaps.encryption && window.currentFolderCaps.encryption.encrypted);
|
||||
const msg = inEncrypted
|
||||
? `You selected ${mapped.length} files. In encrypted folders, downloads are limited to ${limit} files at a time.`
|
||||
: (t('too_many_plain_downloads') || `You selected ${mapped.length} files. For more than ${limit} files, please use "Download as ZIP".`);
|
||||
: (t('too_many_plain_downloads') || `You selected ${mapped.length} files. For more than ${limit} files, please use "Download as Archive".`);
|
||||
showToast(msg, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// fileMenu.js
|
||||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||||
import { updateRowHighlight } from './domUtils.js?v={{APP_QVER}}';
|
||||
import { updateRowHighlight, escapeHTML, isArchiveFileName } from './domUtils.js?v={{APP_QVER}}';
|
||||
import {
|
||||
handleDeleteSelected, handleCopySelected, handleMoveSelected,
|
||||
handleDownloadZipSelected, handleExtractZipSelected,
|
||||
@@ -10,9 +10,6 @@ import { previewFile, buildPreviewUrl, openShareModal } from './filePreview.js?v
|
||||
import { editFile } from './fileEditor.js?v={{APP_QVER}}';
|
||||
import { canEditFile, fileData, downloadSelectedFilesIndividually, startInlineRenameFromContext } from './fileListView.js?v={{APP_QVER}}';
|
||||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||||
import { escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||||
|
||||
|
||||
const MENU_ID = 'fileContextMenu';
|
||||
|
||||
function qMenu() { return document.getElementById(MENU_ID); }
|
||||
@@ -137,7 +134,7 @@ function currentSelection() {
|
||||
const any = files.length > 0;
|
||||
const one = files.length === 1;
|
||||
const many = files.length > 1;
|
||||
const anyZip = files.some(f => f.name.toLowerCase().endsWith('.zip'));
|
||||
const anyZip = files.some(f => isArchiveFileName(f.name));
|
||||
const file = one ? files[0] : null;
|
||||
const canEditFlag = !!(file && canEditFile(file.name));
|
||||
|
||||
|
||||
+30
-25
@@ -61,8 +61,8 @@ const translations = {
|
||||
"copy_selected": "Copy Selected",
|
||||
"move_selected": "Move Selected",
|
||||
"tag_selected": "Tag Selected",
|
||||
"download_zip": "Download Zip",
|
||||
"extract_zip": "Extract Zip",
|
||||
"download_zip": "Download Archive",
|
||||
"extract_zip": "Extract Archive",
|
||||
"preview": "Preview",
|
||||
"preview_too_large": "Preview too large to display.",
|
||||
"edit": "Edit",
|
||||
@@ -118,9 +118,10 @@ const translations = {
|
||||
"move_files_title": "Move Selected Files",
|
||||
"move_files_message": "Select a target folder for moving the selected files:",
|
||||
"move": "Move",
|
||||
"extract_zip_button": "Extract Zip",
|
||||
"download_zip_title": "Download Selected Files as Zip",
|
||||
"download_zip_prompt": "Enter a name for the zip file:",
|
||||
"extract_zip_button": "Extract Archive",
|
||||
"download_zip_title": "Download Selected Files as Archive",
|
||||
"download_zip_prompt": "Enter a name for the archive file:",
|
||||
"download_archive_format": "Archive format",
|
||||
"zip_placeholder": "files.zip",
|
||||
"share": "Share",
|
||||
"total_files": "Total Files",
|
||||
@@ -533,8 +534,8 @@ const translations = {
|
||||
"copy_selected": "Copiar seleccionados",
|
||||
"move_selected": "Mover seleccionados",
|
||||
"tag_selected": "Etiquetar seleccionados",
|
||||
"download_zip": "Descargar Zip",
|
||||
"extract_zip": "Extraer Zip",
|
||||
"download_zip": "Descargar archivo",
|
||||
"extract_zip": "Extraer archivo",
|
||||
"preview": "Vista previa",
|
||||
"edit": "Editar",
|
||||
"rename": "Renombrar",
|
||||
@@ -588,9 +589,10 @@ const translations = {
|
||||
"move_files_title": "Mover archivos seleccionados",
|
||||
"move_files_message": "Seleccione una carpeta destino para mover los archivos seleccionados:",
|
||||
"move": "Mover",
|
||||
"extract_zip_button": "Extraer Zip",
|
||||
"download_zip_title": "Descargar archivos seleccionados en un Zip",
|
||||
"download_zip_prompt": "Ingrese un nombre para el archivo Zip:",
|
||||
"extract_zip_button": "Extraer archivo",
|
||||
"download_zip_title": "Descargar archivos seleccionados en un archivo",
|
||||
"download_zip_prompt": "Ingrese un nombre para el archivo:",
|
||||
"download_archive_format": "Formato de archivo",
|
||||
"zip_placeholder": "files.zip",
|
||||
|
||||
// Login Form keys:
|
||||
@@ -772,8 +774,8 @@ const translations = {
|
||||
"copy_selected": "Copier la sélection",
|
||||
"move_selected": "Déplacer la sélection",
|
||||
"tag_selected": "Étiqueter la sélection",
|
||||
"download_zip": "Télécharger le Zip",
|
||||
"extract_zip": "Extraire le Zip",
|
||||
"download_zip": "Télécharger l'archive",
|
||||
"extract_zip": "Extraire l'archive",
|
||||
"preview": "Aperçu",
|
||||
"edit": "Modifier",
|
||||
"rename": "Renommer",
|
||||
@@ -826,9 +828,10 @@ const translations = {
|
||||
"move_files_title": "Déplacer les fichiers sélectionnés",
|
||||
"move_files_message": "Sélectionnez un dossier de destination pour déplacer les fichiers sélectionnés :",
|
||||
"move": "Déplacer",
|
||||
"extract_zip_button": "Extraire le Zip",
|
||||
"download_zip_title": "Télécharger les fichiers sélectionnés en Zip",
|
||||
"download_zip_prompt": "Entrez un nom pour le fichier Zip :",
|
||||
"extract_zip_button": "Extraire l'archive",
|
||||
"download_zip_title": "Télécharger les fichiers sélectionnés en archive",
|
||||
"download_zip_prompt": "Entrez un nom pour le fichier d'archive :",
|
||||
"download_archive_format": "Format d'archive",
|
||||
"zip_placeholder": "files.zip",
|
||||
|
||||
// Login Form keys:
|
||||
@@ -1010,8 +1013,8 @@ const translations = {
|
||||
"copy_selected": "Ausgewählte kopieren",
|
||||
"move_selected": "Ausgewählte verschieben",
|
||||
"tag_selected": "Ausgewählte taggen",
|
||||
"download_zip": "Zip herunterladen",
|
||||
"extract_zip": "Zip entpacken",
|
||||
"download_zip": "Archiv herunterladen",
|
||||
"extract_zip": "Archiv entpacken",
|
||||
"preview": "Vorschau",
|
||||
"edit": "Bearbeiten",
|
||||
"rename": "Umbenennen",
|
||||
@@ -1065,9 +1068,10 @@ const translations = {
|
||||
"move_files_title": "Ausgewählte Dateien verschieben",
|
||||
"move_files_message": "Wählen Sie einen Zielordner, um die ausgewählten Dateien zu verschieben:",
|
||||
"move": "Verschieben",
|
||||
"extract_zip_button": "Zip entpacken",
|
||||
"download_zip_title": "Ausgewählte Dateien als Zip herunterladen",
|
||||
"download_zip_prompt": "Geben Sie einen Namen für die Zip-Datei ein:",
|
||||
"extract_zip_button": "Archiv entpacken",
|
||||
"download_zip_title": "Ausgewählte Dateien als Archiv herunterladen",
|
||||
"download_zip_prompt": "Geben Sie einen Namen für die Archivdatei ein:",
|
||||
"download_archive_format": "Archivformat",
|
||||
"zip_placeholder": "files.zip",
|
||||
"share": "Teilen",
|
||||
"total_files": "Gesamtanzahl",
|
||||
@@ -1259,8 +1263,8 @@ const translations = {
|
||||
"copy_selected": "复制所选",
|
||||
"move_selected": "移动所选",
|
||||
"tag_selected": "标记所选",
|
||||
"download_zip": "下载 ZIP",
|
||||
"extract_zip": "解压 ZIP",
|
||||
"download_zip": "下载压缩包",
|
||||
"extract_zip": "解压压缩包",
|
||||
"preview": "预览",
|
||||
"edit": "编辑",
|
||||
"rename": "重命名",
|
||||
@@ -1312,9 +1316,10 @@ const translations = {
|
||||
"move_files_title": "移动所选文件",
|
||||
"move_files_message": "选择目标文件夹以移动所选文件:",
|
||||
"move": "移动",
|
||||
"extract_zip_button": "解压 ZIP",
|
||||
"download_zip_title": "将所选文件打包为 ZIP 下载",
|
||||
"download_zip_prompt": "输入 ZIP 文件名:",
|
||||
"extract_zip_button": "解压压缩包",
|
||||
"download_zip_title": "将所选文件打包为压缩包下载",
|
||||
"download_zip_prompt": "输入压缩包文件名:",
|
||||
"download_archive_format": "压缩格式",
|
||||
"zip_placeholder": "files.zip",
|
||||
"share": "分享",
|
||||
"total_files": "文件总数",
|
||||
|
||||
@@ -1386,6 +1386,18 @@ function bindDarkMode() {
|
||||
alert('Login failed');
|
||||
});
|
||||
}
|
||||
function focusLoginUsername() {
|
||||
const input = document.getElementById('loginUsername');
|
||||
const form = document.getElementById('authForm');
|
||||
if (!input || !form || input.disabled) return;
|
||||
if (form.hasAttribute('hidden') || form.style.display === 'none') return;
|
||||
try {
|
||||
if (getComputedStyle(form).display === 'none') return;
|
||||
} catch (e) { }
|
||||
requestAnimationFrame(() => {
|
||||
try { input.focus(); } catch (e) { }
|
||||
});
|
||||
}
|
||||
function afterLogin() {
|
||||
// If index.html was opened with ?redirect=<url>, honor that first
|
||||
try {
|
||||
@@ -1641,6 +1653,7 @@ function bindDarkMode() {
|
||||
keepCreateDropdownWired();
|
||||
wireModalEnterDefault();
|
||||
showLoginTip('Please log in to continue');
|
||||
focusLoginUsername();
|
||||
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
}, { once: true });
|
||||
|
||||
+151
-53
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../../config/config.php';
|
||||
require __DIR__ . '/../../src/models/FileModel.php';
|
||||
require __DIR__ . '/../../src/lib/SourceContext.php';
|
||||
require_once __DIR__ . '/../../src/lib/SourceContext.php';
|
||||
|
||||
$token = $argv[1] ?? '';
|
||||
$token = preg_replace('/[^a-f0-9]/','',$token);
|
||||
@@ -57,6 +57,35 @@ $touchPhase = function(string $phase) use (&$job, $save) {
|
||||
$save();
|
||||
};
|
||||
|
||||
$format = strtolower((string)($job['format'] ?? 'zip'));
|
||||
if (!in_array($format, ['zip', '7z'], true)) {
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Unsupported archive format.';
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] error: ".$job['error']."\n", FILE_APPEND);
|
||||
exit(0);
|
||||
}
|
||||
$job['format'] = $format;
|
||||
|
||||
$findBin = function(array $candidates): ?string {
|
||||
foreach ($candidates as $bin) {
|
||||
if ($bin === '') continue;
|
||||
if (str_contains($bin, '/')) {
|
||||
if (is_file($bin) && is_executable($bin)) {
|
||||
return $bin;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$out = [];
|
||||
$rc = 1;
|
||||
@exec('command -v ' . escapeshellarg($bin) . ' 2>/dev/null', $out, $rc);
|
||||
if ($rc === 0 && !empty($out[0])) {
|
||||
return trim($out[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Init timing
|
||||
if (empty($job['startedAt'])) {
|
||||
$job['startedAt'] = time();
|
||||
@@ -101,7 +130,7 @@ try {
|
||||
$fp = $folderPathReal . DIRECTORY_SEPARATOR . $bn;
|
||||
if (is_file($fp)) $filesToZip[] = $fp;
|
||||
}
|
||||
if (!$filesToZip) throw new RuntimeException('No valid files to zip.');
|
||||
if (!$filesToZip) throw new RuntimeException('No valid files to archive.');
|
||||
|
||||
// Totals for progress
|
||||
$filesTotal = count($filesToZip);
|
||||
@@ -120,71 +149,140 @@ try {
|
||||
$job['phase'] = 'zipping';
|
||||
$save();
|
||||
|
||||
// Create final zip path in META_DIR/ziptmp
|
||||
$zipName = 'download-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.zip';
|
||||
$zipPath = $root . DIRECTORY_SEPARATOR . $zipName;
|
||||
if ($format === 'zip') {
|
||||
// Create final zip path in META_DIR/ziptmp
|
||||
$zipName = 'download-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.zip';
|
||||
$zipPath = $root . DIRECTORY_SEPARATOR . $zipName;
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new RuntimeException('Could not create zip archive.');
|
||||
}
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new RuntimeException('Could not create zip archive.');
|
||||
}
|
||||
|
||||
// Enumerate files; report up to 98%
|
||||
$bytesDone = 0;
|
||||
$filesDone = 0;
|
||||
foreach ($filesToZip as $fp) {
|
||||
$bn = basename($fp);
|
||||
$zip->addFile($fp, $bn);
|
||||
// Enumerate files; report up to 98%
|
||||
$bytesDone = 0;
|
||||
$filesDone = 0;
|
||||
foreach ($filesToZip as $fp) {
|
||||
$bn = basename($fp);
|
||||
$zip->addFile($fp, $bn);
|
||||
|
||||
$filesDone++;
|
||||
$sz = @filesize($fp);
|
||||
if ($sz !== false) $bytesDone += (int)$sz;
|
||||
$filesDone++;
|
||||
$sz = @filesize($fp);
|
||||
if ($sz !== false) $bytesDone += (int)$sz;
|
||||
|
||||
$job['filesDone'] = $filesDone;
|
||||
$job['bytesDone'] = $bytesDone;
|
||||
$job['current'] = $bn;
|
||||
$job['filesDone'] = $filesDone;
|
||||
$job['bytesDone'] = $bytesDone;
|
||||
$job['current'] = $bn;
|
||||
|
||||
$pct = ($bytesTotal > 0) ? (int) floor(($bytesDone / $bytesTotal) * 98) : 0;
|
||||
if ($pct < 0) $pct = 0;
|
||||
if ($pct > 98) $pct = 98;
|
||||
if ($pct > (int)($job['pct'] ?? 0)) $job['pct'] = $pct;
|
||||
$pct = ($bytesTotal > 0) ? (int) floor(($bytesDone / $bytesTotal) * 98) : 0;
|
||||
if ($pct < 0) $pct = 0;
|
||||
if ($pct > 98) $pct = 98;
|
||||
if ($pct > (int)($job['pct'] ?? 0)) $job['pct'] = $pct;
|
||||
|
||||
$save();
|
||||
}
|
||||
|
||||
// Finalizing (this is where libzip writes & renames)
|
||||
$job['pct'] = max((int)($job['pct'] ?? 0), 99);
|
||||
$job['phase'] = 'finalizing';
|
||||
$job['finalizeAt'] = time();
|
||||
|
||||
// Publish selected totals for a truthful UI during finalizing,
|
||||
// and clear incremental fields so the UI doesn't show "7/7 14 GB / 14 GB" prematurely.
|
||||
$job['selectedFiles'] = $filesTotal;
|
||||
$job['selectedBytes'] = $bytesTotal;
|
||||
$job['filesDone'] = null;
|
||||
$job['bytesDone'] = null;
|
||||
$job['current'] = null;
|
||||
|
||||
$save();
|
||||
|
||||
// ---- finalize the zip on disk ----
|
||||
$ok = $zip->close();
|
||||
$statusStr = method_exists($zip, 'getStatusString') ? $zip->getStatusString() : '';
|
||||
|
||||
if (!$ok || !is_file($zipPath)) {
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Failed to finalize ZIP' . ($statusStr ? " ($statusStr)" : '');
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] error: ".$job['error']."\n", FILE_APPEND);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$job['status'] = 'done';
|
||||
$job['zipPath'] = $zipPath;
|
||||
$job['pct'] = 100;
|
||||
$job['phase'] = 'finalized';
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] done zip={$zipPath}\n", FILE_APPEND);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Finalizing (this is where libzip writes & renames)
|
||||
$job['pct'] = max((int)($job['pct'] ?? 0), 99);
|
||||
$job['phase'] = 'finalizing';
|
||||
$job['finalizeAt'] = time();
|
||||
$archiveExt = ($format === '7z') ? '7z' : 'zip';
|
||||
$archiveName = 'download-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.' . $archiveExt;
|
||||
$archivePath = $root . DIRECTORY_SEPARATOR . $archiveName;
|
||||
|
||||
// Publish selected totals for a truthful UI during finalizing,
|
||||
// and clear incremental fields so the UI doesn't show "7/7 14 GB / 14 GB" prematurely.
|
||||
$job['selectedFiles'] = $filesTotal;
|
||||
$job['selectedBytes'] = $bytesTotal;
|
||||
$job['filesDone'] = null;
|
||||
$job['bytesDone'] = null;
|
||||
$job['current'] = null;
|
||||
$listFile = tempnam($root, '7zlist-');
|
||||
if ($listFile === false) {
|
||||
throw new RuntimeException('Failed to prepare archive file list.');
|
||||
}
|
||||
|
||||
$save();
|
||||
$relNames = [];
|
||||
foreach ($filesToZip as $fp) {
|
||||
$relNames[] = basename($fp);
|
||||
}
|
||||
if (file_put_contents($listFile, implode("\n", $relNames) . "\n", LOCK_EX) === false) {
|
||||
@unlink($listFile);
|
||||
throw new RuntimeException('Failed to write archive file list.');
|
||||
}
|
||||
|
||||
// ---- finalize the zip on disk ----
|
||||
$ok = $zip->close();
|
||||
$statusStr = method_exists($zip, 'getStatusString') ? $zip->getStatusString() : '';
|
||||
|
||||
if (!$ok || !is_file($zipPath)) {
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Failed to finalize ZIP' . ($statusStr ? " ($statusStr)" : '');
|
||||
$job['pct'] = max((int)($job['pct'] ?? 0), 99);
|
||||
$job['phase'] = 'finalizing';
|
||||
$job['finalizeAt'] = time();
|
||||
$job['selectedFiles'] = $filesTotal;
|
||||
$job['selectedBytes'] = $bytesTotal;
|
||||
$job['filesDone'] = null;
|
||||
$job['bytesDone'] = null;
|
||||
$job['current'] = null;
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] error: ".$job['error']."\n", FILE_APPEND);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$job['status'] = 'done';
|
||||
$job['zipPath'] = $zipPath;
|
||||
$job['pct'] = 100;
|
||||
$job['phase'] = 'finalized';
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] done zip={$zipPath}\n", FILE_APPEND);
|
||||
$cwd = getcwd();
|
||||
if ($cwd !== false) {
|
||||
@chdir($folderPathReal);
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$rc = 1;
|
||||
$bin = $findBin(['7zz', '/usr/bin/7zz', '/usr/local/bin/7zz', '/bin/7zz', '7z', '/usr/bin/7z', '/usr/local/bin/7z', '/bin/7z']);
|
||||
if (!$bin) {
|
||||
throw new RuntimeException('7z is not available on the server.');
|
||||
}
|
||||
$workArg = '-w' . $root;
|
||||
$cmd = escapeshellarg($bin) . ' a -t7z -y -bd ' . escapeshellarg($workArg) . ' ' . escapeshellarg($archivePath) . ' ' . escapeshellarg('@' . $listFile);
|
||||
@exec($cmd, $out, $rc);
|
||||
|
||||
if ($cwd !== false) {
|
||||
@chdir($cwd);
|
||||
}
|
||||
@unlink($listFile);
|
||||
|
||||
if ($rc !== 0 || !is_file($archivePath)) {
|
||||
$detail = trim(implode("\n", $out));
|
||||
if (strlen($detail) > 200) $detail = substr($detail, 0, 200) . '...';
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Failed to create archive' . ($detail ? ': ' . $detail : '');
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] error: ".$job['error']."\n", FILE_APPEND);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$job['status'] = 'done';
|
||||
$job['zipPath'] = $archivePath;
|
||||
$job['pct'] = 100;
|
||||
$job['phase'] = 'finalized';
|
||||
$save();
|
||||
file_put_contents($logFile, "[".date('c')."] done {$format}={$archivePath}\n", FILE_APPEND);
|
||||
exit(0);
|
||||
} catch (Throwable $e) {
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Worker exception: '.$e->getMessage();
|
||||
|
||||
@@ -2046,6 +2046,18 @@ class FileController
|
||||
'finalizeAt' => $job['finalizeAt'] ?? null,
|
||||
];
|
||||
|
||||
if (($job['status'] ?? '') === 'queued') {
|
||||
$queuedAt = (int)($job['ctime'] ?? 0);
|
||||
if ($queuedAt > 0 && empty($job['startedAt'])) {
|
||||
$age = time() - $queuedAt;
|
||||
if ($age > 20) {
|
||||
$out['status'] = 'error';
|
||||
$out['error'] = 'Archive worker did not start. Check server PHP CLI and permissions.';
|
||||
$out['ready'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($ready) {
|
||||
$out['size'] = @filesize($job['zipPath']) ?: null;
|
||||
$out['downloadUrl'] = '/api/file/downloadZipFile.php?k=' . urlencode($token);
|
||||
@@ -2115,14 +2127,38 @@ class FileController
|
||||
@ini_set('output_buffering', 'off');
|
||||
while (ob_get_level() > 0) @ob_end_clean();
|
||||
|
||||
$format = strtolower((string)($job['format'] ?? 'zip'));
|
||||
if (!in_array($format, ['zip', '7z'], true)) {
|
||||
@unlink($zipReal);
|
||||
http_response_code(400);
|
||||
echo "Unsupported archive format.";
|
||||
return;
|
||||
}
|
||||
$ext = ($format === '7z') ? '7z' : 'zip';
|
||||
$mimeMap = [
|
||||
'zip' => 'application/zip',
|
||||
'7z' => 'application/x-7z-compressed',
|
||||
];
|
||||
$mimeType = $mimeMap[$format] ?? 'application/octet-stream';
|
||||
|
||||
@clearstatcache(true, $zipReal);
|
||||
$name = isset($_GET['name']) ? preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$_GET['name']) : 'files.zip';
|
||||
if ($name === '' || str_ends_with($name, '.')) $name = 'files.zip';
|
||||
$name = isset($_GET['name']) ? preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$_GET['name']) : 'files.' . $ext;
|
||||
if ($name === '' || str_ends_with($name, '.')) $name = 'files';
|
||||
$lower = strtolower($name);
|
||||
foreach (['.zip', '.7z'] as $suffix) {
|
||||
if (str_ends_with($lower, $suffix)) {
|
||||
$name = substr($name, 0, -strlen($suffix));
|
||||
break;
|
||||
}
|
||||
}
|
||||
$name = rtrim($name, '.');
|
||||
if ($name === '') $name = 'files';
|
||||
$name .= '.' . $ext;
|
||||
$size = (int)@filesize($zipReal);
|
||||
|
||||
header('X-Accel-Buffering: no');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Disposition: attachment; filename="' . $name . '"');
|
||||
if ($size > 0) header('Content-Length: ' . $size);
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
@@ -2146,7 +2182,7 @@ class FileController
|
||||
|
||||
$storage = StorageRegistry::getAdapter();
|
||||
if (!$storage->isLocal()) {
|
||||
$this->_jsonOut(["error" => "ZIP operations are not supported for remote storage."], 400);
|
||||
$this->_jsonOut(["error" => "Archive operations are not supported for remote storage."], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2163,13 +2199,50 @@ class FileController
|
||||
return;
|
||||
}
|
||||
|
||||
$format = strtolower(trim((string)($data['format'] ?? 'zip')));
|
||||
if ($format === '') {
|
||||
$format = 'zip';
|
||||
}
|
||||
$allowedFormats = ['zip', '7z'];
|
||||
if (!in_array($format, $allowedFormats, true)) {
|
||||
$msg = "Invalid archive format.";
|
||||
$this->_jsonOut(["error" => $msg], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$findBin = function (array $candidates): ?string {
|
||||
foreach ($candidates as $bin) {
|
||||
if ($bin === '') continue;
|
||||
if (str_contains($bin, '/')) {
|
||||
if (is_file($bin) && is_executable($bin)) {
|
||||
return $bin;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$out = [];
|
||||
$rc = 1;
|
||||
@exec('command -v ' . escapeshellarg($bin) . ' 2>/dev/null', $out, $rc);
|
||||
if ($rc === 0 && !empty($out[0])) {
|
||||
return trim($out[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if ($format === '7z') {
|
||||
$bin = $findBin(['7zz', '/usr/bin/7zz', '/usr/local/bin/7zz', '/bin/7zz', '7z', '/usr/bin/7z', '/usr/local/bin/7z', '/bin/7z']);
|
||||
if (!$bin) {
|
||||
$this->_jsonOut(["error" => "7z is not available on the server; cannot create 7z archives."], 400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$username = $_SESSION['username'] ?? '';
|
||||
$perms = $this->loadPerms($username);
|
||||
$sourceId = class_exists('SourceContext') ? SourceContext::getActiveId() : '';
|
||||
|
||||
// Optional zip gate by account flag
|
||||
if (!$this->isAdmin($perms) && !empty($perms['disableZip'])) {
|
||||
$this->_jsonOut(["error" => "ZIP downloads are not allowed for your account."], 403);
|
||||
$this->_jsonOut(["error" => "Archive downloads are not allowed for your account."], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2210,7 +2283,7 @@ class FileController
|
||||
@chmod($tokDir, 0700);
|
||||
@chmod($logDir, 0700);
|
||||
if (!is_dir($tokDir) || !is_writable($tokDir)) {
|
||||
$this->_jsonOut(["error" => "ZIP token dir not writable."], 500);
|
||||
$this->_jsonOut(["error" => "Archive token dir not writable."], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2237,17 +2310,50 @@ class FileController
|
||||
foreach ($tokens as $tf) {
|
||||
$job = json_decode((string)@file_get_contents($tf), true) ?: [];
|
||||
$st = $job['status'] ?? 'unknown';
|
||||
$pid = (int)($job['spawn']['pid'] ?? 0);
|
||||
$tokenKey = pathinfo($tf, PATHINFO_FILENAME);
|
||||
$pidAlive = false;
|
||||
$pidCmdChecked = false;
|
||||
$pidLooksLikeWorker = false;
|
||||
if ($pid > 0) {
|
||||
if (is_dir('/proc/' . $pid)) {
|
||||
$pidAlive = true;
|
||||
$cmdline = @file_get_contents('/proc/' . $pid . '/cmdline');
|
||||
if (is_string($cmdline) && $cmdline !== '') {
|
||||
$pidCmdChecked = true;
|
||||
$cmdline = str_replace("\0", ' ', $cmdline);
|
||||
if (str_contains($cmdline, 'zip_worker.php') && ($tokenKey === '' || str_contains($cmdline, $tokenKey))) {
|
||||
$pidLooksLikeWorker = true;
|
||||
}
|
||||
}
|
||||
} elseif (function_exists('posix_kill')) {
|
||||
$pidAlive = @posix_kill($pid, 0);
|
||||
}
|
||||
}
|
||||
$queuedAt = (int)($job['ctime'] ?? 0);
|
||||
$startedAt = (int)($job['startedAt'] ?? 0);
|
||||
$queuedAge = ($queuedAt > 0) ? ($now - $queuedAt) : 0;
|
||||
|
||||
$staleQueued = ($st === 'queued' && $startedAt <= 0 && $queuedAge > 120);
|
||||
$staleWorkingNoPid = (in_array($st, ['working', 'finalizing'], true) && $pid <= 0 && $queuedAge > 120);
|
||||
$staleRunning = (in_array($st, ['working', 'finalizing'], true) && $pid > 0 && !$pidAlive);
|
||||
$stalePidMismatch = (in_array($st, ['working', 'finalizing'], true) && $pid > 0 && $pidAlive && $pidCmdChecked && !$pidLooksLikeWorker && $queuedAge > 120);
|
||||
if ($staleQueued || $staleWorkingNoPid || $staleRunning || $stalePidMismatch) {
|
||||
@unlink($tf);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($st === 'queued' || $st === 'working' || $st === 'finalizing') {
|
||||
$all++;
|
||||
if (($job['user'] ?? '') === $username) $mine++;
|
||||
}
|
||||
}
|
||||
if ($mine >= $perUserCap) {
|
||||
$this->_jsonOut(["error" => "You already have ZIP jobs running. Try again shortly."], 429);
|
||||
$this->_jsonOut(["error" => "You already have archive jobs running. Try again shortly."], 429);
|
||||
return;
|
||||
}
|
||||
if ($all >= $globalCap) {
|
||||
$this->_jsonOut(["error" => "ZIP queue is busy. Try again shortly."], 429);
|
||||
$this->_jsonOut(["error" => "Archive queue is busy. Try again shortly."], 429);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2259,6 +2365,7 @@ class FileController
|
||||
'folder' => $folder,
|
||||
'files' => array_values($files),
|
||||
'sourceId' => $sourceId,
|
||||
'format' => $format,
|
||||
'status' => 'queued',
|
||||
'ctime' => time(),
|
||||
'startedAt' => null,
|
||||
@@ -2267,7 +2374,7 @@ class FileController
|
||||
'error' => null
|
||||
];
|
||||
if (file_put_contents($tokFile, json_encode($job, JSON_PRETTY_PRINT), LOCK_EX) === false) {
|
||||
$this->_jsonOut(["error" => "Failed to create zip job."], 500);
|
||||
$this->_jsonOut(["error" => "Failed to create archive job."], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2277,7 +2384,7 @@ class FileController
|
||||
$job['status'] = 'error';
|
||||
$job['error'] = 'Spawn failed: ' . $spawn['error'];
|
||||
@file_put_contents($tokFile, json_encode($job, JSON_PRETTY_PRINT), LOCK_EX);
|
||||
$this->_jsonOut(["error" => "Failed to enqueue ZIP: " . $spawn['error']], 500);
|
||||
$this->_jsonOut(["error" => "Failed to enqueue archive: " . $spawn['error']], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2290,7 +2397,7 @@ class FileController
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('FileController::downloadZip enqueue error: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||||
$this->_jsonOut(['error' => 'Internal error while queuing ZIP.'], 500);
|
||||
$this->_jsonOut(['error' => 'Internal error while queuing archive.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2303,7 +2410,7 @@ class FileController
|
||||
|
||||
$storage = StorageRegistry::getAdapter();
|
||||
if (!$storage->isLocal()) {
|
||||
$this->_jsonOut(["error" => "ZIP operations are not supported for remote storage."], 400);
|
||||
$this->_jsonOut(["error" => "Archive operations are not supported for remote storage."], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+890
-93
File diff suppressed because it is too large
Load Diff
@@ -227,4 +227,4 @@ echo "🔥 Starting Apache..."
|
||||
# Stream only the chosen logs; -n0 = don't dump history, -F = follow across rotations/creation
|
||||
[ "${STREAM_ERR}" = "true" ] && tail -n0 -F /var/www/metadata/log/error.log 2>/dev/null &
|
||||
[ "${STREAM_ACC}" = "true" ] && tail -n0 -F /var/www/metadata/log/access.log 2>/dev/null &
|
||||
exec apachectl -D FOREGROUND
|
||||
exec apachectl -D FOREGROUND
|
||||
|
||||
Reference in New Issue
Block a user