diff --git a/deploy/nginx-panel.conf b/deploy/nginx-panel.conf
index 91e6a36..8a15a42 100644
--- a/deploy/nginx-panel.conf
+++ b/deploy/nginx-panel.conf
@@ -14,6 +14,9 @@ http {
sendfile on;
gzip on;
+ # Large uploads via the admin File Manager (e.g. ISOs, installers) can be multi-GB.
+ client_max_body_size 20g;
+
# ── Admin Panel (8882) ─────────────────────────────────────────────────────
server {
listen 8882 ssl;
@@ -43,7 +46,11 @@ http {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /srv/novacpx/public/api/index.php;
fastcgi_param SERVER_PORT 8882;
- fastcgi_read_timeout 300;
+ fastcgi_read_timeout 1800;
+ fastcgi_param PHP_VALUE "upload_max_filesize=20G
+post_max_size=20G
+max_execution_time=1800
+max_input_time=1800";
}
}
diff --git a/panel/assets/js/admin.js b/panel/assets/js/admin.js
index 817b8ef..f6531d3 100644
--- a/panel/assets/js/admin.js
+++ b/panel/assets/js/admin.js
@@ -4802,21 +4802,84 @@ window.fmSubmitChown = (path) => {
});
};
window.fmUpload = () => {
- Nova.modal('Upload File', `
`,
- `
- `);
+ Nova.modal('Upload File', `
+
+ `,
+ `
+ `);
};
+
+function fmFormatBytes(n) {
+ if (n >= 1073741824) return (n / 1073741824).toFixed(2) + ' GB';
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + ' MB';
+ if (n >= 1024) return (n / 1024).toFixed(1) + ' KB';
+ return n + ' B';
+}
+
window.fmSubmitUpload = () => {
const fileInput = document.getElementById('fm-upfile');
- if (!fileInput?.files[0]) return;
- document.querySelector('.modal-overlay')?.remove();
- fmGuard(_fmPath, 'upload a file into', async (confirm_dangerous) => {
+ const file = fileInput?.files[0];
+ if (!file) return;
+ fmGuard(_fmPath, 'upload a file into', (confirm_dangerous) => {
+ // Switch the modal from the file-picker to a progress view; keep it open for the whole transfer.
+ document.getElementById('fm-upload-form').style.display = 'none';
+ document.getElementById('fm-upload-progress').style.display = 'block';
+ document.getElementById('fm-upload-filename').textContent = file.name;
+ document.getElementById('fm-upload-bytes').textContent = '0 B / ' + fmFormatBytes(file.size);
+ document.getElementById('fm-upload-cancel-btn').style.display = 'none';
+ const goBtn = document.getElementById('fm-upload-go-btn');
+ goBtn.disabled = true;
+ goBtn.textContent = 'Uploading…';
+
const fd = new FormData();
- fd.append('file', fileInput.files[0]);
+ fd.append('file', file);
fd.append('path', _fmPath);
fd.append('confirm_dangerous', confirm_dangerous ? '1' : '');
- const res = await fetch(`/api/files/upload?path=${encodeURIComponent(_fmPath)}`, { method: 'POST', credentials: 'include', body: fd }).then(r => r.json());
- if (res?.success) { Nova.toast('Uploaded', 'success'); fmNav(_fmPath); } else Nova.toast(res?.message || 'Upload failed', 'error');
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', `/api/files/upload?path=${encodeURIComponent(_fmPath)}`, true);
+ xhr.withCredentials = true;
+
+ xhr.upload.addEventListener('progress', (e) => {
+ if (!e.lengthComputable) return;
+ const pct = Math.round((e.loaded / e.total) * 100);
+ const bar = document.getElementById('fm-upload-bar');
+ const pctEl = document.getElementById('fm-upload-pct');
+ const bytesEl = document.getElementById('fm-upload-bytes');
+ if (bar) bar.style.width = pct + '%';
+ if (pctEl) pctEl.textContent = pct + '%';
+ if (bytesEl) bytesEl.textContent = fmFormatBytes(e.loaded) + ' / ' + fmFormatBytes(e.total);
+ });
+
+ xhr.onload = () => {
+ let res = null;
+ try { res = JSON.parse(xhr.responseText); } catch (e) { /* non-JSON error page, e.g. nginx 413 */ }
+ if (xhr.status >= 200 && xhr.status < 300 && res?.success) {
+ document.querySelector('.modal-overlay')?.remove();
+ Nova.toast('Uploaded', 'success');
+ fmNav(_fmPath);
+ } else {
+ document.querySelector('.modal-overlay')?.remove();
+ const msg = res?.message || (xhr.status === 413 ? 'File too large for the server\'s current upload limit' : `Upload failed (HTTP ${xhr.status})`);
+ Nova.toast(msg, 'error');
+ }
+ };
+ xhr.onerror = () => {
+ document.querySelector('.modal-overlay')?.remove();
+ Nova.toast('Upload failed — connection error', 'error');
+ };
+ xhr.send(fd);
});
};
window.fmExtract = (path) => {