diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee0fbd..a617d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Changes 10/6/2025 v1.3.15 + +feat/perf: large-file handling, faster file list, richer CodeMirror modes (fixes #48) + +- fileEditor.js: block ≥10 MB; plain-text fallback >5 MB; lighter CM settings for big files. +- fileListView.js: latest-call-wins; compute editable via ext + sizeBytes (no blink). +- FileModel.php: add sizeBytes; cap inline content to ≤5 MB (INDEX_TEXT_BYTES_MAX). +- HTML: load extra CM modes: htmlmixed, php, clike, python, yaml, markdown, shell, sql, vb, ruby, perl, properties, nginx. + +--- + ## Changes 10/5/2025 v1.3.14 fix(admin): OIDC optional by default; validate only when enabled (fixes #44) @@ -9,6 +20,8 @@ fix(admin): OIDC optional by default; validate only when enabled (fixes #44) - AdminController default loginOptions sets disableOIDCLogin=true; CSRF via header or body - Normalize file perms to 0664 after write +--- + ## Changes 10/4/2025 v1.3.13 fix(scanner): resolve dirs via CLI/env/constants; write per-item JSON; skip trash @@ -27,6 +40,8 @@ chore(scanner): skip profile_pics subtree during scans - Avoids indexing internal avatar images (folder already hidden in UI) - Reduces scan noise and metadata churn; keeps firmware/other content indexed +--- + ## Changes 10/4/2025 v1.3.12 Fix: robust PUID/PGID handling; optional ownership normalization (closes #43) @@ -35,6 +50,8 @@ Fix: robust PUID/PGID handling; optional ownership normalization (closes #43) - Added CHOWN_ON_START env to control recursive chown (default true; turn off after first run) - SCAN_ON_START unchanged, with non-root fallback +--- + ## Changes 10/4/2025 v1.3.11 Chore: keep BASE_URL fallback, prefer env SHARE_URL; fix HTTPS auto-detect @@ -42,6 +59,8 @@ Chore: keep BASE_URL fallback, prefer env SHARE_URL; fix HTTPS auto-detect - Remove no-op sed of SHARE_URL from start.sh (env already used) - Build default share link with correct scheme (http/https, proxy-aware) +--- + ## Changes 10/4/2025 v1.3.10 Fix: index externally added files on startup; harden start.sh (#46) @@ -54,6 +73,8 @@ Fix: index externally added files on startup; harden start.sh (#46) No behavior change unless SCAN_ON_START=true. +--- + ## Changes 5/27/2025 v1.3.9 - Support for mounting CIFS (SMB) network shares via Docker volumes diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index 6f534df..3d06105 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; -const version = "v1.3.13"; +const version = "v1.3.15"; const adminTitle = `${t("admin_panel")} ${version}`; // ————— Inject updated styles ————— diff --git a/public/js/fileEditor.js b/public/js/fileEditor.js index ca74af4..741dbd5 100644 --- a/public/js/fileEditor.js +++ b/public/js/fileEditor.js @@ -3,20 +3,143 @@ import { escapeHTML, showToast } from './domUtils.js'; import { loadFileList } from './fileListView.js'; import { t } from './i18n.js'; +// thresholds for editor behavior +const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings +const EDITOR_BLOCK_THRESHOLD = 10 * 1024 * 1024; // >10 MiB => block editing + +// Lazy-load CodeMirror modes on demand +const CM_CDN = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/"; +const MODE_URL = { + // core you've likely already loaded: + "xml": "mode/xml/xml.min.js", + "css": "mode/css/css.min.js", + "javascript": "mode/javascript/javascript.min.js", + + // extras you may want on-demand: + "htmlmixed": "mode/htmlmixed/htmlmixed.min.js", + "application/x-httpd-php": "mode/php/php.min.js", + "php": "mode/php/php.min.js", + "markdown": "mode/markdown/markdown.min.js", + "python": "mode/python/python.min.js", + "sql": "mode/sql/sql.min.js", + "shell": "mode/shell/shell.min.js", + "yaml": "mode/yaml/yaml.min.js", + "properties": "mode/properties/properties.min.js", + "text/x-csrc": "mode/clike/clike.min.js", + "text/x-c++src": "mode/clike/clike.min.js", + "text/x-java": "mode/clike/clike.min.js", + "text/x-csharp": "mode/clike/clike.min.js", + "text/x-kotlin": "mode/clike/clike.min.js" +}; + +function loadScriptOnce(url) { + return new Promise((resolve, reject) => { + const key = `cm:${url}`; + let s = document.querySelector(`script[data-key="${key}"]`); + if (s) { + if (s.dataset.loaded === "1") return resolve(); + s.addEventListener("load", () => resolve()); + s.addEventListener("error", reject); + return; + } + s = document.createElement("script"); + s.src = url; + s.defer = true; + s.dataset.key = key; + s.addEventListener("load", () => { s.dataset.loaded = "1"; resolve(); }); + s.addEventListener("error", reject); + document.head.appendChild(s); + }); +} + +async function ensureModeLoaded(modeOption) { + if (!window.CodeMirror) return; // CM core must be present + const name = typeof modeOption === "string" ? modeOption : (modeOption && modeOption.name); + if (!name) return; + // Already registered? + if ((CodeMirror.modes && CodeMirror.modes[name]) || (CodeMirror.mimeModes && CodeMirror.mimeModes[name])) { + return; + } + const url = MODE_URL[name]; + if (!url) return; // unknown -> fallback to text/plain + // Dependencies (htmlmixed needs xml/css/js; php highlighting with HTML also benefits from htmlmixed) + if (name === "htmlmixed") { + await Promise.all([ + ensureModeLoaded("xml"), + ensureModeLoaded("css"), + ensureModeLoaded("javascript") + ]); + } + if (name === "application/x-httpd-php") { + await ensureModeLoaded("htmlmixed"); + } + await loadScriptOnce(CM_CDN + url); +} + function getModeForFile(fileName) { - const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); + const dot = fileName.lastIndexOf("."); + const ext = dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : ""; + switch (ext) { - case "css": - return "css"; - case "json": - return { name: "javascript", json: true }; - case "js": - return "javascript"; + // markup case "html": case "htm": - return "text/html"; + return "text/html"; // ensureModeLoaded will map to htmlmixed case "xml": return "xml"; + case "md": + case "markdown": + return "markdown"; + case "yml": + case "yaml": + return "yaml"; + + // styles & scripts + case "css": + return "css"; + case "js": + return "javascript"; + case "json": + return { name: "javascript", json: true }; + + // server / langs + case "php": + return "application/x-httpd-php"; + case "py": + return "python"; + case "sql": + return "sql"; + case "sh": + case "bash": + case "zsh": + case "bat": + return "shell"; + + // config-y files + case "ini": + case "conf": + case "config": + case "properties": + return "properties"; + + // C-family / JVM + case "c": + case "h": + return "text/x-csrc"; + case "cpp": + case "cxx": + case "hpp": + case "hh": + case "hxx": + return "text/x-c++src"; + case "java": + return "text/x-java"; + case "cs": + return "text/x-csharp"; + case "kt": + case "kts": + return "text/x-kotlin"; + default: return "text/plain"; } @@ -47,6 +170,7 @@ export function editFile(fileName, folder) { if (existingEditor) { existingEditor.remove(); } + const folderUsed = folder || window.currentFolder || "root"; const folderPath = folderUsed === "root" ? "uploads/" @@ -55,26 +179,40 @@ export function editFile(fileName, folder) { fetch(fileUrl, { method: "HEAD" }) .then(response => { - const contentLength = response.headers.get("Content-Length"); - if (contentLength !== null && parseInt(contentLength) > 10485760) { + const lenHeader = + response.headers.get("content-length") ?? + response.headers.get("Content-Length"); + const sizeBytes = lenHeader ? parseInt(lenHeader, 10) : null; + + if (sizeBytes !== null && sizeBytes > EDITOR_BLOCK_THRESHOLD) { showToast("This file is larger than 10 MB and cannot be edited in the browser."); throw new Error("File too large."); } - return fetch(fileUrl); + return response; }) + .then(() => fetch(fileUrl)) .then(response => { if (!response.ok) { throw new Error("HTTP error! Status: " + response.status); } - return response.text(); + const lenHeader = + response.headers.get("content-length") ?? + response.headers.get("Content-Length"); + const sizeBytes = lenHeader ? parseInt(lenHeader, 10) : null; + return Promise.all([response.text(), sizeBytes]); }) - .then(content => { + .then(([content, sizeBytes]) => { + const forcePlainText = + sizeBytes !== null && sizeBytes > EDITOR_PLAIN_THRESHOLD; + const modal = document.createElement("div"); modal.id = "editorContainer"; modal.classList.add("modal", "editor-modal"); modal.innerHTML = `