mirror of
https://github.com/error311/FileRise.git
synced 2026-05-12 15:00:36 -05:00
release(v3.1.2): configurable ignore rules for indexing/tree + admin UX polish (fixes #91, refs #92)
- add ignoreRegex setting (admin config) with env override FR_IGNORE_REGEX to hide folders from tree/counts/indexing - add snapshot preset helper for common NAS snapshot paths (fixes #91) - unify ignore logic via FS::shouldIgnoreEntry across folder counts, tree listing, and disk usage scans - admin: improve settings search UX (clear button) + smoother section header styling - UI: polish header dock collapse/expand icon animations (landing/lift + reduced-motion support) Fixes #91 Refs #92 Co-authored-by: nikp123 <nikp123@e.email>
This commit is contained in:
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 01/20/2026 (v3.1.2)
|
||||
|
||||
`release(v3.1.2): configurable ignore rules for indexing/tree + admin UX polish (fixes #91, refs #92)`
|
||||
|
||||
**Commit message**
|
||||
|
||||
```text
|
||||
release(v3.1.2): configurable ignore rules for indexing/tree + admin UX polish (fixes #91, refs #92)
|
||||
|
||||
- add ignoreRegex setting (admin config) with env override FR_IGNORE_REGEX to hide folders from tree/counts/indexing
|
||||
- add snapshot preset helper for common NAS snapshot paths (fixes #91)
|
||||
- unify ignore logic via FS::shouldIgnoreEntry across folder counts, tree listing, and disk usage scans
|
||||
- admin: improve settings search UX (clear button) + smoother section header styling
|
||||
- UI: polish header dock collapse/expand icon animations (landing/lift + reduced-motion support)
|
||||
|
||||
Fixes #91
|
||||
Refs #92
|
||||
|
||||
Co-authored-by: nikp123 <nikp123@e.email>
|
||||
```
|
||||
|
||||
**Added**
|
||||
|
||||
- **Indexing ignore rules (regex)**:
|
||||
- Admin setting: **Ignore paths (regex)** (`ignoreRegex`) — one pattern per line.
|
||||
- Env override: `FR_IGNORE_REGEX` (locks the field when set).
|
||||
- Built-in “quick add” button for a common snapshot preset: `(^|/)(@?snapshots?)(/|$)` (helps with NAS snapshot dirs).
|
||||
- **Centralized ignore helper**:
|
||||
- `FS::shouldIgnoreEntry($name, $parentRel)` applies built-in ignores plus optional regex patterns.
|
||||
|
||||
**Changed**
|
||||
|
||||
- **Folder tree / listing / counts now share ignore logic**:
|
||||
- Replaced scattered ignore arrays with `FS::shouldIgnoreEntry(...)` in folder enumeration paths.
|
||||
- **Disk usage scan now filters earlier**:
|
||||
- Uses a `RecursiveCallbackFilterIterator` so ignored entries are skipped before deeper traversal.
|
||||
- **Admin Panel UX**:
|
||||
- Settings search now includes a dedicated clear (X) button that appears only when a query exists.
|
||||
- Section headers now render via a `.section-header-inner` wrapper for cleaner layout/hover/active styles.
|
||||
- Audit table area now caps height and scrolls to avoid huge modal growth.
|
||||
- **Header dock polish**:
|
||||
- Adds “lift” and “land” animations for header dock icon buttons during card collapse/expand.
|
||||
- Respects `prefers-reduced-motion`.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- **#91:** Snapshot folders (e.g., `snapshot`, `@snapshots`) can now be excluded cleanly from the tree, counts, indexing, and disk usage views via ignore rules.
|
||||
- Prevents “stuck landing” icon states by cleaning up animation classes/inline vars on `animationend`.
|
||||
|
||||
**Notes**
|
||||
|
||||
- Ignore rules are applied frequently during tree/list/count operations. Keep patterns simple to avoid expensive regexes.
|
||||
- Invalid regex lines are ignored safely (and won’t crash listing/indexing).
|
||||
|
||||
---
|
||||
|
||||
## Changes 01/17/2026 (v3.1.1)
|
||||
|
||||
`release(v3.1.1): OIDC env overrides + configurable resumable chunk size + clearer startup logs (closes #86, closes #87, closes #90)`
|
||||
|
||||
@@ -185,6 +185,31 @@ if ($envKey === false || $envKey === '') {
|
||||
$encryptionKey = $envKey;
|
||||
}
|
||||
|
||||
// Optional: ignore regex for indexing/listing (env wins; admin config as fallback)
|
||||
if (!defined('FR_IGNORE_REGEX')) {
|
||||
$envIgnore = getenv('FR_IGNORE_REGEX');
|
||||
if ($envIgnore !== false && trim((string)$envIgnore) !== '') {
|
||||
define('FR_IGNORE_REGEX', (string)$envIgnore);
|
||||
} else {
|
||||
$cfgPath = USERS_DIR . 'adminConfig.json';
|
||||
if (is_file($cfgPath)) {
|
||||
$enc = @file_get_contents($cfgPath);
|
||||
if ($enc !== false && $enc !== '') {
|
||||
$dec = decryptData($enc, $encryptionKey);
|
||||
if ($dec !== false) {
|
||||
$cfg = json_decode($dec, true);
|
||||
if (is_array($cfg) && isset($cfg['ignoreRegex']) && is_string($cfg['ignoreRegex'])) {
|
||||
$val = trim($cfg['ignoreRegex']);
|
||||
if ($val !== '') {
|
||||
define('FR_IGNORE_REGEX', $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to load JSON permissions (with optional decryption)
|
||||
function loadUserPermissions($username)
|
||||
{
|
||||
|
||||
+144
-12
@@ -2909,10 +2909,78 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--header-icon-delay: 0ms;
|
||||
--header-icon-land-ms: 120ms;
|
||||
--header-icon-lift-ms: 90ms;
|
||||
}
|
||||
|
||||
.header-card-icon .material-icons {
|
||||
font-size: 22px;
|
||||
transform-origin: center center;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.header-card-icon.is-landing .material-icons {
|
||||
animation: headerIconLand var(--header-icon-land-ms) cubic-bezier(.22,.61,.36,1) var(--header-icon-delay) backwards;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.header-card-icon.is-launching {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-card-icon.is-launching .material-icons {
|
||||
animation: headerIconLift var(--header-icon-lift-ms) ease-in forwards;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@keyframes headerIconLand {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) rotateZ(-2deg);
|
||||
filter: none;
|
||||
}
|
||||
35% {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px) rotateZ(1.5deg);
|
||||
filter: drop-shadow(0 6px 10px rgba(0,0,0,0.28));
|
||||
}
|
||||
65% {
|
||||
transform: translateY(0) rotateZ(-1deg);
|
||||
filter: drop-shadow(0 3px 6px rgba(0,0,0,0.18));
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotateZ(0);
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes headerIconLift {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotateZ(0);
|
||||
filter: none;
|
||||
}
|
||||
35% {
|
||||
transform: translateY(-2px) rotateZ(1.5deg);
|
||||
filter: drop-shadow(0 6px 10px rgba(0,0,0,0.26));
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px) rotateZ(-1deg);
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.header-card-icon.is-landing .material-icons,
|
||||
.header-card-icon.is-launching .material-icons {
|
||||
animation: none !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active {
|
||||
@@ -2954,9 +3022,11 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n
|
||||
.section-content { display:none; margin-left:20px; margin-top:8px; margin-bottom:8px; }
|
||||
|
||||
#adminPanelModal .section-header {
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.05), rgba(15, 23, 42, 0.02));
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
padding: 8px 12px 8px 22px;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -2964,19 +3034,31 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
color: #111827;
|
||||
transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
||||
}
|
||||
#adminPanelModal .section-header:hover {
|
||||
#adminPanelModal .section-header-inner {
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.05), rgba(15, 23, 42, 0.02));
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease, padding-left 140ms ease;
|
||||
}
|
||||
#adminPanelModal .section-header:hover .section-header-inner {
|
||||
background: rgba(15, 23, 42, 0.04);
|
||||
border-color: rgba(15, 23, 42, 0.16);
|
||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
#adminPanelModal .section-header:not(.collapsed) {
|
||||
#adminPanelModal .section-header:not(.collapsed) .section-header-inner {
|
||||
padding-left: 22px;
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
border-color: rgba(59, 130, 246, 0.35);
|
||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
#adminPanelModal .section-header:not(.collapsed)::before {
|
||||
#adminPanelModal .section-header:not(.collapsed) .section-header-inner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
@@ -2987,20 +3069,22 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n
|
||||
background: linear-gradient(180deg, rgba(37, 99, 235, 0.9), rgba(96, 165, 250, 0.9));
|
||||
}
|
||||
#adminPanelModal .section-header .material-icons { color: #475569; }
|
||||
body.dark-mode #adminPanelModal .section-header {
|
||||
body.dark-mode #adminPanelModal .section-header { color: #f1f5f9; }
|
||||
body.dark-mode #adminPanelModal .section-header .section-header-inner {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
body.dark-mode #adminPanelModal .section-header:hover {
|
||||
body.dark-mode #adminPanelModal .section-header:hover .section-header-inner {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
body.dark-mode #adminPanelModal .section-header:not(.collapsed) {
|
||||
body.dark-mode #adminPanelModal .section-header:not(.collapsed) .section-header-inner {
|
||||
background: rgba(96, 165, 250, 0.18);
|
||||
border-color: rgba(96, 165, 250, 0.35);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
body.dark-mode #adminPanelModal .section-header:not(.collapsed)::before {
|
||||
body.dark-mode #adminPanelModal .section-header:not(.collapsed) .section-header-inner::before {
|
||||
background: linear-gradient(180deg, rgba(148, 197, 255, 0.95), rgba(96, 165, 250, 0.95));
|
||||
}
|
||||
body.dark-mode #adminPanelModal .section-header .material-icons { color: #cbd5f5; }
|
||||
@@ -3031,6 +3115,11 @@ body.dark-mode #adminPanelModal .section-content {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
#adminPanelModal .audit-table-wrap {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#adminPanelModal .admin-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3087,6 +3176,39 @@ body.dark-mode #adminPanelModal .section-content {
|
||||
max-height: 180px;
|
||||
transition: max-height 200ms ease, opacity 200ms ease, transform 200ms ease, margin 200ms ease, padding 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
#adminPanelModal .admin-search-input {
|
||||
position: relative;
|
||||
}
|
||||
#adminPanelModal .admin-search-input .form-control {
|
||||
padding-right: 38px;
|
||||
}
|
||||
#adminPanelModal .admin-search-clear {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#adminPanelModal .admin-search-clear:hover {
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
color: #111;
|
||||
}
|
||||
#adminPanelModal .admin-search-clear:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
#adminPanelModal .admin-search-clear .material-icons {
|
||||
font-size: 18px;
|
||||
}
|
||||
#adminPanelModal .admin-search-wrap.is-collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
@@ -3113,6 +3235,16 @@ body.dark-mode #adminPanelModal .admin-search-wrap {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
body.dark-mode #adminPanelModal .admin-search-clear {
|
||||
color: #9ca3af;
|
||||
}
|
||||
body.dark-mode #adminPanelModal .admin-search-clear:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
body.dark-mode #adminPanelModal .admin-search-clear:focus {
|
||||
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.25);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
#adminPanelModal .admin-search-toggle-label { display: none; }
|
||||
#adminPanelModal .admin-panel-header { gap: 8px; }
|
||||
|
||||
+224
-18
@@ -628,6 +628,40 @@ function wireClamavTestButton(scope = document) {
|
||||
});
|
||||
}
|
||||
|
||||
function wireIgnoreRegexPresetButton(scope = document) {
|
||||
const btn = scope.querySelector('#ignoreRegexSnapshotsPreset');
|
||||
const input = scope.querySelector('#ignoreRegex');
|
||||
if (!btn || !input || btn.__wired) return;
|
||||
|
||||
btn.__wired = true;
|
||||
|
||||
const preset = '(^|/)(@?snapshots?)(/|$)';
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (input.disabled) return;
|
||||
|
||||
const current = String(input.value || '');
|
||||
const hasPreset = current
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.some(line => line.trim() === preset);
|
||||
if (hasPreset) {
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.trim() === '') {
|
||||
input.value = preset;
|
||||
} else {
|
||||
const suffix = current.endsWith('\n') ? '' : '\n';
|
||||
input.value = current + suffix + preset;
|
||||
}
|
||||
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function renderAdminEncryptionSection({ config, dark }) {
|
||||
const host = document.getElementById("encryptionContent");
|
||||
if (!host) return;
|
||||
@@ -2341,6 +2375,7 @@ function captureInitialAdminConfig() {
|
||||
hoverPreviewMaxVideoMb: (document.getElementById("hoverPreviewMaxVideoMb")?.value || "").trim(),
|
||||
ffmpegPath: (document.getElementById("ffmpegPath")?.value || "").trim(),
|
||||
fileListSummaryDepth: (document.getElementById("fileListSummaryDepth")?.value || "").trim(),
|
||||
ignoreRegex: (document.getElementById("ignoreRegex")?.value || "").trim(),
|
||||
|
||||
clamavScanUploads: !!document.getElementById("clamavScanUploads")?.checked,
|
||||
proSearchEnabled: !!document.getElementById("proSearchEnabled")?.checked,
|
||||
@@ -2386,6 +2421,7 @@ function hasUnsavedChanges() {
|
||||
getVal("hoverPreviewMaxVideoMb") !== (o.hoverPreviewMaxVideoMb || "") ||
|
||||
getVal("ffmpegPath") !== (o.ffmpegPath || "") ||
|
||||
getVal("fileListSummaryDepth") !== (o.fileListSummaryDepth || "") ||
|
||||
getVal("ignoreRegex") !== (o.ignoreRegex || "") ||
|
||||
getChk("clamavScanUploads") !== o.clamavScanUploads ||
|
||||
getChk("proSearchEnabled") !== o.proSearchEnabled ||
|
||||
getVal("proSearchLimit") !== o.proSearchLimit ||
|
||||
@@ -2542,6 +2578,10 @@ function clearSectionTimer(cnt) {
|
||||
clearTimeout(cnt.__closeTimer);
|
||||
cnt.__closeTimer = null;
|
||||
}
|
||||
if (cnt && cnt.__focusTimer) {
|
||||
clearTimeout(cnt.__focusTimer);
|
||||
cnt.__focusTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openSectionContent(cnt) {
|
||||
@@ -2581,6 +2621,60 @@ function setSectionContentImmediate(cnt, open) {
|
||||
}
|
||||
}
|
||||
|
||||
function focusSectionIntoView(hdr, cnt) {
|
||||
if (!hdr || !cnt) return;
|
||||
const scroller = hdr.closest(".modal-content") || document.scrollingElement || document.documentElement;
|
||||
if (!scroller) {
|
||||
try {
|
||||
hdr.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
|
||||
} catch (e) {
|
||||
hdr.scrollIntoView();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const padding = 12;
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const headerRect = hdr.getBoundingClientRect();
|
||||
const targetTop = scroller.scrollTop + (headerRect.top - scrollerRect.top) - padding;
|
||||
const nextTop = Math.max(0, targetTop);
|
||||
if (Math.abs(nextTop - scroller.scrollTop) < 1) return;
|
||||
try {
|
||||
if (typeof scroller.scrollTo === "function") {
|
||||
scroller.scrollTo({ top: nextTop, behavior: "smooth" });
|
||||
setTimeout(() => {
|
||||
if (Math.abs(scroller.scrollTop - nextTop) > 1) {
|
||||
scroller.scrollTop = nextTop;
|
||||
}
|
||||
}, 40);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback for browsers that don't accept scroll options.
|
||||
}
|
||||
scroller.scrollTop = nextTop;
|
||||
}
|
||||
|
||||
function scheduleSectionFocus(hdr, cnt) {
|
||||
if (!hdr || !cnt) return;
|
||||
clearSectionTimer(cnt);
|
||||
const focusNow = () => {
|
||||
if (cnt.style.display === "none") return;
|
||||
focusSectionIntoView(hdr, cnt);
|
||||
};
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
focusNow();
|
||||
cnt.__focusTimer = setTimeout(() => {
|
||||
if (cnt.classList.contains("is-open") && cnt.style.display !== "none") {
|
||||
focusSectionIntoView(hdr, cnt);
|
||||
}
|
||||
cnt.__focusTimer = null;
|
||||
}, SECTION_ANIM_MS + 40);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSection(id) {
|
||||
const hdr = document.getElementById(id + "Header");
|
||||
const cnt = document.getElementById(id + "Content");
|
||||
@@ -2590,6 +2684,7 @@ function toggleSection(id) {
|
||||
closeSectionContent(cnt);
|
||||
} else {
|
||||
openSectionContent(cnt);
|
||||
scheduleSectionFocus(hdr, cnt);
|
||||
}
|
||||
if (!isCollapsedNow && id === "shareLinks") {
|
||||
loadShareLinksSection();
|
||||
@@ -2609,6 +2704,7 @@ function wireAdminPanelSearch(sectionIds) {
|
||||
const emptyState = document.getElementById("adminSettingsSearchEmpty");
|
||||
const wrap = document.getElementById("adminSettingsSearchWrap");
|
||||
const toggleBtn = document.getElementById("adminSearchToggle");
|
||||
const clearBtn = document.getElementById("adminSearchClear");
|
||||
if (!input || !Array.isArray(sectionIds)) return;
|
||||
|
||||
const ids = sectionIds.filter(Boolean);
|
||||
@@ -2623,9 +2719,14 @@ function wireAdminPanelSearch(sectionIds) {
|
||||
};
|
||||
|
||||
const updateToggleState = () => {
|
||||
if (!toggleBtn) return;
|
||||
const hasQuery = !!normalizeAdminSearchText(input.value);
|
||||
toggleBtn.classList.toggle("is-active", hasQuery);
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.toggle("is-active", hasQuery);
|
||||
}
|
||||
if (clearBtn) {
|
||||
clearBtn.hidden = !hasQuery;
|
||||
clearBtn.disabled = !hasQuery;
|
||||
}
|
||||
};
|
||||
|
||||
const restoreState = () => {
|
||||
@@ -2681,6 +2782,25 @@ function wireAdminPanelSearch(sectionIds) {
|
||||
updateToggleState();
|
||||
};
|
||||
|
||||
const clearSearch = (opts = {}) => {
|
||||
input.value = "";
|
||||
applyFilter();
|
||||
if (opts.collapse) {
|
||||
setOpen(false);
|
||||
}
|
||||
if (opts.focus) {
|
||||
input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
if (clearBtn && !clearBtn.__wired) {
|
||||
clearBtn.__wired = true;
|
||||
clearBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
clearSearch({ focus: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleBtn && wrap && !toggleBtn.__wired) {
|
||||
toggleBtn.__wired = true;
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
@@ -2691,7 +2811,7 @@ function wireAdminPanelSearch(sectionIds) {
|
||||
}
|
||||
const hasQuery = !!normalizeAdminSearchText(input.value);
|
||||
if (hasQuery) {
|
||||
input.focus();
|
||||
clearSearch({ collapse: true });
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
@@ -3816,6 +3936,10 @@ export function openAdminPanel() {
|
||||
const ffmpegPathValue = ffmpegPathLockedByEnv ? ffmpegPathEffective : ffmpegPathCfg;
|
||||
const ffmpegHelpDefault = 'Used for video thumbnail generation. Leave blank to use ffmpeg from PATH.';
|
||||
const ffmpegHelpLocked = 'Controlled by container env FR_FFMPEG_PATH. Change it in your Docker/host env.';
|
||||
const ignoreRegexCfg = (typeof config.ignoreRegex === 'string') ? config.ignoreRegex : '';
|
||||
const ignoreRegexEffective = (typeof config.ignoreRegexEffective === 'string') ? config.ignoreRegexEffective : '';
|
||||
const ignoreRegexLockedByEnv = !!config.ignoreRegexLockedByEnv;
|
||||
const ignoreRegexValue = ignoreRegexLockedByEnv ? ignoreRegexEffective : ignoreRegexCfg;
|
||||
const supportedLanguages = [
|
||||
{ code: 'en', labelKey: 'english', fallback: 'English' },
|
||||
{ code: 'es', labelKey: 'spanish', fallback: 'Español' },
|
||||
@@ -3870,7 +3994,7 @@ export function openAdminPanel() {
|
||||
`;
|
||||
const sections = [
|
||||
{ id: "userManagement", label: tf("users_access", "Users & Access") },
|
||||
{ id: "headerSettings", label: tf("appearance_ui", "Appearance & UI") },
|
||||
{ id: "headerSettings", label: tf("appearance_ui", "Appearance, UI & Indexing") },
|
||||
{ id: "loginOptions", label: tf("auth_webdav", "Auth & WebDAV (OIDC/TOTP)") },
|
||||
{ id: "upload", label: tf("uploads_antivirus", "Uploads & Antivirus") },
|
||||
{ id: "shareLinks", label: tf("sharing_links", "Sharing & Links") },
|
||||
@@ -3911,13 +4035,25 @@ export function openAdminPanel() {
|
||||
</div>
|
||||
<form id="adminPanelForm">
|
||||
<div id="adminSettingsSearchWrap" class="form-group admin-search-wrap is-collapsed">
|
||||
<input
|
||||
type="text"
|
||||
id="adminSettingsSearch"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
placeholder="${tf("settings_search_placeholder", "Search settings...")}"
|
||||
/>
|
||||
<div class="admin-search-input">
|
||||
<input
|
||||
type="text"
|
||||
id="adminSettingsSearch"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
placeholder="${tf("settings_search_placeholder", "Search settings...")}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="adminSearchClear"
|
||||
class="admin-search-clear"
|
||||
aria-label="${tf("clear_search", "Clear search")}"
|
||||
title="${tf("clear_search", "Clear search")}"
|
||||
hidden
|
||||
>
|
||||
<i class="material-icons" aria-hidden="true">close</i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">
|
||||
${tf("settings_search_help", "Type to filter sections and settings.")}
|
||||
</small>
|
||||
@@ -3927,7 +4063,9 @@ export function openAdminPanel() {
|
||||
</div>
|
||||
${sections.map(sec => `
|
||||
<div id="${sec.id}Header" class="section-header collapsed">
|
||||
${sec.label} <i class="material-icons">expand_more</i>
|
||||
<div class="section-header-inner">
|
||||
${sec.label} <i class="material-icons">expand_more</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="${sec.id}Content" class="section-content"></div>
|
||||
`).join("")}
|
||||
@@ -4239,6 +4377,44 @@ export function openAdminPanel() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="admin-subsection-title" style="margin-top:2px;">
|
||||
${tf("indexing_ignore_rules", "Indexing ignore rules")}
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:8px;">
|
||||
<label for="ignoreRegex">${tf("ignore_regex_label", "Ignore paths (regex)")}</label>
|
||||
<textarea
|
||||
id="ignoreRegex"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="(^|/)(@?snapshots?)(/|$)"
|
||||
${ignoreRegexLockedByEnv ? "disabled data-locked='1'" : ""}>${escapeHTML(ignoreRegexValue || "")}</textarea>
|
||||
<small class="text-muted d-block mt-1">
|
||||
${tf("ignore_regex_help", "One pattern per line. Matches entry name or relative path from root (no leading slash; e.g. \"projects/snapshot/2024\").")}
|
||||
</small>
|
||||
<small class="text-muted d-block mt-1">
|
||||
${tf("ignore_regex_scope_note", "Affects folder tree, counts, and indexing. Dot-prefixed entries (like .snapshots) are already hidden; use this for non-dot snapshot folders (snapshot, @snapshots).")}
|
||||
</small>
|
||||
<small class="text-muted d-block mt-1">
|
||||
Built-in ignores: dot-prefixed entries (including .snapshots), <code>@eaDir</code>, <code>#recycle</code>,
|
||||
<code>.DS_Store</code>, <code>Thumbs.db</code>, <code>trash</code>, <code>profile_pics</code>.
|
||||
</small>
|
||||
<div class="d-flex flex-wrap align-items-center" style="gap:8px; margin-top:6px;">
|
||||
<span class="text-muted small">Quick add:</span>
|
||||
<button
|
||||
type="button"
|
||||
id="ignoreRegexSnapshotsPreset"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
${ignoreRegexLockedByEnv ? "disabled" : ""}>
|
||||
Add snapshot preset
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">
|
||||
${ignoreRegexLockedByEnv
|
||||
? `Env <code>FR_IGNORE_REGEX</code> overrides and locks this field.`
|
||||
: `Env <code>FR_IGNORE_REGEX</code> overrides this field when set.`}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<hr class="admin-divider">
|
||||
|
||||
<!-- Pro: Footer text -->
|
||||
@@ -4645,9 +4821,13 @@ export function openAdminPanel() {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
wireClamavTestButton(document.getElementById("uploadContent"));
|
||||
const uploadScope = document.getElementById("uploadContent");
|
||||
const headerSettingsScope = document.getElementById("headerSettingsContent");
|
||||
wireIgnoreRegexPresetButton(headerSettingsScope);
|
||||
wireClamavTestButton(uploadScope);
|
||||
initVirusLogUI({ isPro });
|
||||
// ONLYOFFICE section (moved into adminOnlyOffice.js)
|
||||
initOnlyOfficeUI({ config });
|
||||
@@ -4993,7 +5173,7 @@ ${t("shared_max_upload_size_bytes")}
|
||||
${!isPro ? `
|
||||
<div style="margin-top:8px;">
|
||||
<a
|
||||
href="https://filerise.net/pro/checkout.php"
|
||||
href="https://filerise.net/pro/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-pro-admin"
|
||||
@@ -5346,7 +5526,13 @@ ${t("shared_max_upload_size_bytes")}
|
||||
const pill = (!isPro || !proSearchApiOk || !proAuditAvailable)
|
||||
? '<span class="btn-pro-pill" style="position:static; display:inline-flex; align-items:center; margin:0;">Pro</span>'
|
||||
: '';
|
||||
proFeaturesHeaderEl.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">Pro Features${pill}</span> ${iconHtml}`;
|
||||
const labelHtml = `<span style="display:inline-flex;align-items:center;gap:6px;">Pro Features${pill}</span> ${iconHtml}`;
|
||||
const inner = proFeaturesHeaderEl.querySelector('.section-header-inner');
|
||||
if (inner) {
|
||||
inner.innerHTML = labelHtml;
|
||||
} else {
|
||||
proFeaturesHeaderEl.innerHTML = `<div class="section-header-inner">${labelHtml}</div>`;
|
||||
}
|
||||
}
|
||||
if (proFeaturesContainer) {
|
||||
const proSearchBlockedReason = !isPro ? 'pro' : (!proSearchApiOk ? 'api' : null);
|
||||
@@ -5476,7 +5662,7 @@ ${t("shared_max_upload_size_bytes")}
|
||||
|
||||
<div id="auditStatus" class="text-muted" style="font-size:12px; margin-bottom:8px;"></div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive audit-table-wrap">
|
||||
<table class="table table-sm" style="margin-bottom:0;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -5901,6 +6087,16 @@ ${t("shared_max_upload_size_bytes")}
|
||||
help.textContent = locked ? ffmpegHelpLocked : ffmpegHelpDefault;
|
||||
}
|
||||
}
|
||||
const ignoreEl = document.getElementById("ignoreRegex");
|
||||
if (ignoreEl) {
|
||||
const locked = !!config.ignoreRegexLockedByEnv;
|
||||
const val = locked
|
||||
? (config.ignoreRegexEffective || "")
|
||||
: (config.ignoreRegex || "");
|
||||
ignoreEl.value = (val || "").toString();
|
||||
ignoreEl.disabled = locked;
|
||||
ignoreEl.dataset.locked = locked ? "1" : "0";
|
||||
}
|
||||
// --- ClamAV toggle wiring (refresh) ---
|
||||
const cfgClam = config.clamav || {};
|
||||
const clamChk = document.getElementById("clamavScanUploads");
|
||||
@@ -5925,7 +6121,10 @@ ${t("shared_max_upload_size_bytes")}
|
||||
}
|
||||
}
|
||||
}
|
||||
wireClamavTestButton(document.getElementById("uploadContent"));
|
||||
const uploadScope = document.getElementById("uploadContent");
|
||||
const headerSettingsScope = document.getElementById("headerSettingsContent");
|
||||
wireIgnoreRegexPresetButton(headerSettingsScope);
|
||||
wireClamavTestButton(uploadScope);
|
||||
initVirusLogUI({ isPro });
|
||||
renderAdminEncryptionSection({ config, dark });
|
||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig?.providerUrl || "";
|
||||
@@ -5968,7 +6167,13 @@ ${t("shared_max_upload_size_bytes")}
|
||||
const pill = (!isPro || !proSearchApiOk || !proAuditAvailable)
|
||||
? '<span class="btn-pro-pill" style="position:static; display:inline-flex; align-items:center; margin:0;">Pro</span>'
|
||||
: '';
|
||||
pfHeader.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">Pro Features${pill}</span> ${iconHtml}`;
|
||||
const labelHtml = `<span style="display:inline-flex;align-items:center;gap:6px;">Pro Features${pill}</span> ${iconHtml}`;
|
||||
const inner = pfHeader.querySelector('.section-header-inner');
|
||||
if (inner) {
|
||||
inner.innerHTML = labelHtml;
|
||||
} else {
|
||||
pfHeader.innerHTML = `<div class="section-header-inner">${labelHtml}</div>`;
|
||||
}
|
||||
}
|
||||
const psToggle = document.getElementById("proSearchEnabled");
|
||||
const psLimit = document.getElementById("proSearchLimit");
|
||||
@@ -6043,6 +6248,7 @@ function handleSave() {
|
||||
if (el.dataset.locked === "1") return el.value || "";
|
||||
return (el.value || "").trim();
|
||||
})(),
|
||||
ignoreRegex: (document.getElementById("ignoreRegex")?.value || "").trim(),
|
||||
loginOptions: {
|
||||
// Backend still expects “disable*” flags:
|
||||
disableFormLogin: !enableFormLogin,
|
||||
|
||||
@@ -317,6 +317,14 @@ function insertCardInHeader(card) {
|
||||
}
|
||||
});
|
||||
|
||||
iconButton.addEventListener('animationend', (event) => {
|
||||
if (event.animationName === 'headerIconLand') {
|
||||
iconButton.classList.remove('is-landing');
|
||||
iconButton.style.removeProperty('--header-icon-delay');
|
||||
iconButton.style.removeProperty('--header-icon-land-ms');
|
||||
}
|
||||
});
|
||||
|
||||
host.appendChild(iconButton);
|
||||
// make sure the dock is visible when icons exist
|
||||
showHeaderDockPersistent();
|
||||
@@ -503,6 +511,13 @@ const COLLAPSE_OPACITY_END = 0.06;
|
||||
const COLLAPSE_RISE_MS = 120;
|
||||
const COLLAPSE_RISE_PX = 14;
|
||||
const COLLAPSE_RISE_SCALE = 1.02;
|
||||
const HEADER_ICON_LAND_MS = 120;
|
||||
const HEADER_ICON_LIFT_MS = 90;
|
||||
const HEADER_ICON_LAND_EXTRA_DELAY_MS = 40;
|
||||
const HEADER_ICON_LAND_DELAY_MS = Math.max(
|
||||
0,
|
||||
COLLAPSE_ANIMATION_MS - HEADER_ICON_LAND_MS + HEADER_ICON_LAND_EXTRA_DELAY_MS
|
||||
);
|
||||
|
||||
function animateCardsIntoHeaderAndThen(done) {
|
||||
const sb = getSidebar();
|
||||
@@ -545,6 +560,17 @@ function animateCardsIntoHeaderAndThen(done) {
|
||||
const iconBtn = card.headerIconButton;
|
||||
if (!iconBtn) return;
|
||||
|
||||
iconBtn.classList.remove('is-launching');
|
||||
iconBtn.classList.add('is-landing');
|
||||
iconBtn.style.setProperty('--header-icon-delay', `${HEADER_ICON_LAND_DELAY_MS}ms`);
|
||||
iconBtn.style.setProperty('--header-icon-land-ms', `${HEADER_ICON_LAND_MS}ms`);
|
||||
setTimeout(() => {
|
||||
if (!iconBtn.isConnected) return;
|
||||
iconBtn.classList.remove('is-landing');
|
||||
iconBtn.style.removeProperty('--header-icon-delay');
|
||||
iconBtn.style.removeProperty('--header-icon-land-ms');
|
||||
}, HEADER_ICON_LAND_DELAY_MS + HEADER_ICON_LAND_MS + 40);
|
||||
|
||||
const iconRect = iconBtn.getBoundingClientRect();
|
||||
|
||||
const iconName = card.id === 'uploadCard'
|
||||
@@ -739,6 +765,11 @@ function animateCardsOutOfHeaderThen(done) {
|
||||
const fromCx = iconRect.left + iconRect.width / 2;
|
||||
const fromCy = iconRect.bottom + START_OFFSET_Y;
|
||||
|
||||
iconBtn.classList.remove('is-landing');
|
||||
iconBtn.classList.add('is-launching');
|
||||
iconBtn.style.setProperty('--header-icon-lift-ms', `${HEADER_ICON_LIFT_MS}ms`);
|
||||
iconBtn.style.setProperty('--header-icon-delay', '0ms');
|
||||
|
||||
const savedW = parseFloat(card.dataset.lastWidth || '');
|
||||
const savedH = parseFloat(card.dataset.lastHeight || '');
|
||||
const targetWidth = (!Number.isNaN(savedW) && savedW > 0) ? savedW : Math.min(280, Math.max(220, zoneRect.width * 0.85));
|
||||
|
||||
@@ -303,6 +303,11 @@ class AdminController
|
||||
$ffmpegCfg = (string)($config['ffmpegPath'] ?? '');
|
||||
$ffmpegEffective = $ffmpegLockedByEnv ? trim((string)$envFfmpeg) : $ffmpegCfg;
|
||||
|
||||
$envIgnore = getenv('FR_IGNORE_REGEX');
|
||||
$ignoreLockedByEnv = ($envIgnore !== false && trim((string)$envIgnore) !== '');
|
||||
$ignoreCfg = (string)($config['ignoreRegex'] ?? '');
|
||||
$ignoreEffective = $ignoreLockedByEnv ? trim((string)$envIgnore) : $ignoreCfg;
|
||||
|
||||
$adminExtra = [
|
||||
'loginOptions' => array_merge($public['loginOptions'], [
|
||||
'authBypass' => (bool)($config['loginOptions']['authBypass'] ?? false),
|
||||
@@ -338,6 +343,9 @@ class AdminController
|
||||
'ffmpegPath' => $ffmpegCfg,
|
||||
'ffmpegPathEffective' => $ffmpegEffective,
|
||||
'ffmpegPathLockedByEnv' => $ffmpegLockedByEnv,
|
||||
'ignoreRegex' => $ignoreCfg,
|
||||
'ignoreRegexEffective' => $ignoreEffective,
|
||||
'ignoreRegexLockedByEnv' => $ignoreLockedByEnv,
|
||||
'encryption' => [
|
||||
'supported' => (bool)$encSupported,
|
||||
'hasMasterKey'=> (bool)$encHasMasterKey,
|
||||
@@ -1741,6 +1749,7 @@ class AdminController
|
||||
'globalOtpauthUrl' => '',
|
||||
'enableWebDAV' => false,
|
||||
'sharedMaxUploadSize' => 0,
|
||||
'ignoreRegex' => '',
|
||||
'uploads' => [
|
||||
'resumableChunkMb' => 1.5,
|
||||
],
|
||||
@@ -1817,6 +1826,18 @@ class AdminController
|
||||
$merged['ffmpegPath'] = $path;
|
||||
}
|
||||
|
||||
// ignoreRegex: optional ignore patterns (newline-delimited). Env FR_IGNORE_REGEX locks this field.
|
||||
$envIgnore = getenv('FR_IGNORE_REGEX');
|
||||
$ignoreLockedByEnv = ($envIgnore !== false && trim((string)$envIgnore) !== '');
|
||||
if (!$ignoreLockedByEnv && array_key_exists('ignoreRegex', $data)) {
|
||||
$raw = str_replace(["\r\n", "\r"], "\n", trim((string)$data['ignoreRegex']));
|
||||
$raw = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $raw);
|
||||
if (strlen($raw) > 2000) {
|
||||
$raw = substr($raw, 0, 2000);
|
||||
}
|
||||
$merged['ignoreRegex'] = $raw;
|
||||
}
|
||||
|
||||
// loginOptions: inherit existing then override if provided
|
||||
foreach (['disableFormLogin', 'disableBasicAuth', 'disableOIDCLogin', 'authBypass'] as $flag) {
|
||||
if (isset($data['loginOptions'][$flag])) {
|
||||
|
||||
+80
-3
@@ -17,6 +17,84 @@ final class FS
|
||||
return ['trash','profile_pics'];
|
||||
}
|
||||
|
||||
/** Optional regex patterns for additional ignores (env FR_IGNORE_REGEX). */
|
||||
private static function ignoreRegexes(): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$raw = '';
|
||||
if (defined('FR_IGNORE_REGEX')) {
|
||||
$raw = (string)FR_IGNORE_REGEX;
|
||||
} else {
|
||||
$env = getenv('FR_IGNORE_REGEX');
|
||||
$raw = ($env !== false) ? (string)$env : '';
|
||||
}
|
||||
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
$cache = [];
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$patterns = [];
|
||||
$lines = preg_split('/\r?\n/', $raw) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string)$line);
|
||||
if ($line === '') continue;
|
||||
$pattern = self::normalizeIgnoreRegex($line);
|
||||
if ($pattern === null) continue;
|
||||
if (@preg_match($pattern, '') === false) {
|
||||
error_log('FR_IGNORE_REGEX ignored invalid pattern.');
|
||||
continue;
|
||||
}
|
||||
$patterns[] = $pattern;
|
||||
}
|
||||
|
||||
$cache = $patterns;
|
||||
return $cache;
|
||||
}
|
||||
|
||||
private static function normalizeIgnoreRegex(string $raw): ?string {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') return null;
|
||||
|
||||
$delim = $raw[0] ?? '';
|
||||
if ($delim !== '' && !ctype_alnum($delim) && $delim !== '\\') {
|
||||
$quoted = preg_quote($delim, '/');
|
||||
if (preg_match('/^' . $quoted . '.+' . $quoted . '[imsxuADU]*$/', $raw)) {
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
|
||||
$wrap = '~';
|
||||
$safe = str_replace($wrap, '\\' . $wrap, $raw);
|
||||
return $wrap . $safe . $wrap;
|
||||
}
|
||||
|
||||
public static function shouldIgnoreEntry(string $name, string $parentRel = ''): bool {
|
||||
if ($name === '') return false;
|
||||
if (in_array($name, self::IGNORE(), true)) return true;
|
||||
|
||||
$regexes = self::ignoreRegexes();
|
||||
if (!$regexes) return false;
|
||||
|
||||
$prefix = str_replace('\\', '/', trim((string)$parentRel));
|
||||
if ($prefix === '' || strtolower($prefix) === 'root') {
|
||||
$path = $name;
|
||||
} else {
|
||||
$prefix = trim($prefix, '/');
|
||||
$path = $prefix === '' ? $name : ($prefix . '/' . $name);
|
||||
}
|
||||
|
||||
foreach ($regexes as $rx) {
|
||||
if (preg_match($rx, $name) === 1) return true;
|
||||
if ($path !== $name && preg_match($rx, $path) === 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function isSafeSegment(string $name): bool {
|
||||
if ($name === '.' || $name === '..') return false;
|
||||
if (strpos($name, '/') !== false || strpos($name, '\\') !== false) return false;
|
||||
@@ -50,14 +128,13 @@ final class FS
|
||||
): bool {
|
||||
if ($maxDepth <= 0 || !is_dir($absPath)) return false;
|
||||
|
||||
$IGNORE = self::IGNORE();
|
||||
$SKIP = self::SKIP();
|
||||
|
||||
$items = @scandir($absPath) ?: [];
|
||||
foreach ($items as $child) {
|
||||
if ($child === '.' || $child === '..') continue;
|
||||
if ($child[0] === '.') continue;
|
||||
if (in_array($child, $IGNORE, true)) continue;
|
||||
if (self::shouldIgnoreEntry($child, $relPath)) continue;
|
||||
if (!self::isSafeSegment($child)) continue;
|
||||
|
||||
$lower = strtolower($child);
|
||||
@@ -84,4 +161,4 @@ final class FS
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,6 +536,11 @@ class AdminModel
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore regex (optional)
|
||||
$configUpdate['ignoreRegex'] = self::sanitizeIgnoreRegex(
|
||||
$configUpdate['ignoreRegex'] ?? ''
|
||||
);
|
||||
|
||||
// Convert configuration to JSON.
|
||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||
if ($plainTextConfig === false) {
|
||||
@@ -588,6 +593,17 @@ class AdminModel
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function sanitizeIgnoreRegex($value): string
|
||||
{
|
||||
$value = str_replace(["\r\n", "\r"], "\n", trim((string)$value));
|
||||
if ($value === '') return '';
|
||||
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
|
||||
if (strlen($value) > 2000) {
|
||||
$value = substr($value, 0, 2000);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current configuration.
|
||||
*
|
||||
@@ -824,6 +840,12 @@ class AdminModel
|
||||
$config['ffmpegPath'] = $path;
|
||||
}
|
||||
|
||||
if (!isset($config['ignoreRegex']) || !is_string($config['ignoreRegex'])) {
|
||||
$config['ignoreRegex'] = '';
|
||||
} else {
|
||||
$config['ignoreRegex'] = self::sanitizeIgnoreRegex($config['ignoreRegex']);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
@@ -882,6 +904,7 @@ class AdminModel
|
||||
],
|
||||
'publishedUrl' => '',
|
||||
'ffmpegPath' => '',
|
||||
'ignoreRegex' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,6 @@ class DiskUsageModel
|
||||
}
|
||||
$root = rtrim($root, DIRECTORY_SEPARATOR);
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
// Folder map: key => [
|
||||
@@ -252,12 +251,46 @@ class DiskUsageModel
|
||||
|
||||
$rootLen = strlen($root);
|
||||
|
||||
$dirIter = new RecursiveDirectoryIterator(
|
||||
$root,
|
||||
FilesystemIterator::SKIP_DOTS
|
||||
| FilesystemIterator::FOLLOW_SYMLINKS
|
||||
);
|
||||
$filter = new RecursiveCallbackFilterIterator(
|
||||
$dirIter,
|
||||
function (SplFileInfo $current) use ($rootLen, $SKIP): bool {
|
||||
$name = $current->getFilename();
|
||||
if ($name === '.' || $name === '..') return false;
|
||||
if ($name !== '' && $name[0] === '.') return false;
|
||||
if (!FS::isSafeSegment($name)) return false;
|
||||
|
||||
$path = $current->getPathname();
|
||||
$rel = substr($path, $rootLen);
|
||||
$rel = str_replace('\\', '/', $rel);
|
||||
$rel = ltrim($rel, '/');
|
||||
|
||||
$parentRel = dirname($rel);
|
||||
if ($parentRel === '.' || $parentRel === DIRECTORY_SEPARATOR) {
|
||||
$parentRel = '';
|
||||
}
|
||||
if (FS::shouldIgnoreEntry($name, $parentRel)) return false;
|
||||
|
||||
$lowerRel = strtolower($rel);
|
||||
if ($lowerRel === 'trash' || strpos($lowerRel, 'trash/') === 0) {
|
||||
return false;
|
||||
}
|
||||
if ($lowerRel === 'profile_pics' || strpos($lowerRel, 'profile_pics/') === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$baseLower = strtolower(basename($rel));
|
||||
if (in_array($baseLower, $SKIP, true)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(
|
||||
$root,
|
||||
FilesystemIterator::SKIP_DOTS
|
||||
| FilesystemIterator::FOLLOW_SYMLINKS
|
||||
),
|
||||
$filter,
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
@@ -274,10 +307,6 @@ class DiskUsageModel
|
||||
}
|
||||
|
||||
// Skip system/ignored entries
|
||||
if (in_array($name, $IGNORE, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Relative path under UPLOAD_DIR, normalized with '/'
|
||||
$rel = substr($path, $rootLen);
|
||||
$rel = str_replace('\\', '/', $rel);
|
||||
|
||||
+17
-22
@@ -86,8 +86,7 @@ class FolderModel
|
||||
$relPrefix = implode('/', $parts);
|
||||
}
|
||||
|
||||
$IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db'];
|
||||
$SKIP = ['trash', 'profile_pics'];
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
$entries = @scandir($dir);
|
||||
if ($entries === false) {
|
||||
@@ -112,7 +111,7 @@ class FolderModel
|
||||
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
if ($name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $relPrefix)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
if (!self::isSafeSegment($name)) continue;
|
||||
|
||||
@@ -222,8 +221,7 @@ class FolderModel
|
||||
$relPrefix = implode('/', $parts);
|
||||
}
|
||||
|
||||
$IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db'];
|
||||
$SKIP = ['trash', 'profile_pics'];
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
$folderCount = 0;
|
||||
$fileCount = 0;
|
||||
@@ -251,7 +249,7 @@ class FolderModel
|
||||
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
if ($name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $curRel)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
if (!self::isSafeSegment($name)) continue;
|
||||
|
||||
@@ -347,7 +345,6 @@ class FolderModel
|
||||
return ['folders' => 0, 'files' => 0, 'bytes' => 0];
|
||||
}
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
$folderCount = 0;
|
||||
@@ -359,7 +356,7 @@ class FolderModel
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..' || $name === '') continue;
|
||||
if ($name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $folder)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
|
||||
@@ -443,7 +440,6 @@ class FolderModel
|
||||
: $base . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder);
|
||||
$startRel = ($folder === 'root') ? '' : $folder;
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
$folderCount = 0;
|
||||
@@ -469,7 +465,7 @@ class FolderModel
|
||||
|
||||
if ($name === '.' || $name === '..' || $name === '') continue;
|
||||
if ($name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $curRel)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
|
||||
@@ -581,7 +577,6 @@ class FolderModel
|
||||
if ($dirReal === null || !is_dir($dirReal)) return ['items'=>[], 'nextCursor'=>null];
|
||||
}
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP(); // lowercased names to skip (e.g. 'trash', 'profile_pics')
|
||||
|
||||
$entries = @scandir($dirReal);
|
||||
@@ -591,7 +586,7 @@ class FolderModel
|
||||
foreach ($entries as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if ($item[0] === '.') continue;
|
||||
if (in_array($item, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($item, $relPrefix)) continue;
|
||||
if (!FS::isSafeSegment($item)) continue;
|
||||
|
||||
$lower = strtolower($item);
|
||||
@@ -624,6 +619,7 @@ class FolderModel
|
||||
$name = $child->getFilename();
|
||||
if (!$name) continue;
|
||||
if ($name[0] === '.') continue;
|
||||
if (FS::shouldIgnoreEntry($name, $rel)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
|
||||
@@ -710,7 +706,6 @@ class FolderModel
|
||||
? $base
|
||||
: $base . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder);
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
$entries = $storage->list($dirPath);
|
||||
@@ -720,7 +715,7 @@ class FolderModel
|
||||
foreach ($entries as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if ($item === '' || $item[0] === '.') continue;
|
||||
if (in_array($item, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($item, $folder)) continue;
|
||||
if (!FS::isSafeSegment($item)) continue;
|
||||
|
||||
$lower = strtolower($item);
|
||||
@@ -794,13 +789,12 @@ class FolderModel
|
||||
$entries = $storage->list($dirPath);
|
||||
if (!$entries) return false;
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
foreach ($entries as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if ($item === '' || $item[0] === '.') continue;
|
||||
if (in_array($item, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($item, $folder)) continue;
|
||||
if (!FS::isSafeSegment($item)) continue;
|
||||
if (in_array(strtolower($item), $SKIP, true)) continue;
|
||||
|
||||
@@ -1054,7 +1048,6 @@ class FolderModel
|
||||
$fileCount = 0;
|
||||
$totalBytes = 0;
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
while ($queue) {
|
||||
@@ -1080,7 +1073,7 @@ class FolderModel
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
if ($name === '' || $name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $rel)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
|
||||
$lower = strtolower($name);
|
||||
@@ -1707,9 +1700,13 @@ class FolderModel
|
||||
{
|
||||
$folders = [];
|
||||
$items = @scandir($dir) ?: [];
|
||||
$SKIP = FS::SKIP();
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if ($item === '' || $item[0] === '.') continue;
|
||||
if (FS::shouldIgnoreEntry($item, $relative)) continue;
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $item)) continue;
|
||||
if (in_array(strtolower($item), $SKIP, true)) continue;
|
||||
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
if (is_dir($path)) {
|
||||
@@ -1810,7 +1807,6 @@ class FolderModel
|
||||
$scanned = 0;
|
||||
$queue = [['root', $base]];
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
while ($queue && $scanned < $maxFolders) {
|
||||
@@ -1821,7 +1817,7 @@ class FolderModel
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
if ($name === '' || $name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $rel)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
|
||||
@@ -1888,13 +1884,12 @@ class FolderModel
|
||||
return $folderInfoList;
|
||||
}
|
||||
|
||||
$IGNORE = FS::IGNORE();
|
||||
$SKIP = FS::SKIP();
|
||||
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
if ($name === '' || $name[0] === '.') continue;
|
||||
if (in_array($name, $IGNORE, true)) continue;
|
||||
if (FS::shouldIgnoreEntry($name, $parentRel)) continue;
|
||||
if (!FS::isSafeSegment($name)) continue;
|
||||
if (in_array(strtolower($name), $SKIP, true)) continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user