Skip to content

chore: Merge upstream 2.54.0#86

Merged
laurynasgadl merged 179 commits into
masterfrom
chore/merge-upstream-2.54.0
Jan 14, 2026
Merged

chore: Merge upstream 2.54.0#86
laurynasgadl merged 179 commits into
masterfrom
chore/merge-upstream-2.54.0

Conversation

@laurynasgadl
Copy link
Copy Markdown
Member

@laurynasgadl laurynasgadl commented Jan 14, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added CSV file preview with customizable column separator
    • Added Ace editor theme customization setting
    • Added redirect-after-copy/move user preference
    • Added hide login button global setting
    • Implemented enhanced file search with real-time streaming
    • Added inactivity-based session timeout
  • Improvements

    • Enhanced file editor save workflow
    • Improved upload synchronization
    • Expanded language support (Bulgarian, Croatian, Latvian)
    • Enhanced authentication with current password verification
  • Updates

    • Updated Go toolchain and dependencies
    • Updated base container images

✏️ Tip: You can customize this high-level summary in your review settings.

cuiweixie and others added 30 commits September 13, 2025 07:57
94% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
100% translated source file: 'frontend/src/i18n/en.json'
on 'no'.
Co-authored-by: Henrique Dias <mail@hacdias.com>
…nity MB/s (filebrowser#5456)

Co-authored-by: Henrique Dias <mail@hacdias.com>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Ryan Miller <ryan.miller@infinitetactics.com>
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
bjackman and others added 21 commits December 28, 2025 22:04
Co-authored-by: Henrique Dias <mail@hacdias.com>
…lebrowser#5637)

Co-authored-by: FadedAtlas <fadedatlas.shield181@slmail.me>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

This pull request introduces significant architectural and feature changes including CI/CD workflow restructuring (GitHub Actions), build system migration from Make to Task, widespread package renamings (http→fbhttp, errors→fberrors), major configuration system refactoring with Viper-based setup, new frontend features (CSV viewer, context menu, logout page handling), dependency upgrades (Go 1.24→1.25, frontend packages), removal of legacy systems (upgrade command, importer, tests), and substantial documentation reorganization.

Changes

Cohort / File(s) Summary
CI/CD Workflows
.github/workflows/ci.yaml, .github/workflows/docs.yml, .github/workflows/main.yaml
New multi-job CI pipeline (lint-frontend, lint-backend, test, build, release) and docs workflow replacing legacy main.yaml; release job uses GoReleaser with multi-arch Docker support
Build System Overhaul
Taskfile.yml, Makefile, common.mk, tools.mk
Introduced Task-based build orchestration with frontend/backend/docs tasks; simplified Makefile; removed common.mk and tools.mk for tool management
Go Package Renamings
http/*.go, errors/errors.go, imports in multiple files
Renamed package http→fbhttp and errors→fberrors; updated all references and import aliases across ~30 files
Configuration System Refactor
cmd/root.go, cmd/config.go, cmd/utils.go, settings/*.go, storage/bolt/*.go
Introduced Viper-based config handling, new store wrapper pattern replacing pythonData, flag migration mechanism, centralized getSettings function; deprecated direct storage access in command handlers
Frontend Features: CSV & Viewer
frontend/src/utils/csv.ts, frontend/src/components/files/CsvViewer.vue, frontend/src/views/files/Preview.vue, frontend/src/views/Files.vue
New CSV parsing utility and viewer component with separator selection; added CSV preview support in file preview with size validation (5MB limit)
Frontend Features: Context Menu
frontend/src/components/files/FileListing.vue, frontend/src/components/ContextMenu.vue, frontend/src/stores/contextMenu.ts
Migrated from store-based context menu to component-based approach; added context menu positioning and empty-area click handling in file listing
Frontend Settings & Auth
frontend/src/views/settings/Profile.vue, frontend/src/views/settings/User.vue, frontend/src/views/settings/Global.vue, frontend/src/views/Login.vue
Added new user preferences (redirectAfterCopyMove, aceEditorTheme); introduced currentPassword field for auth operations; logout reason display on login page
Search API Streaming
http/search.go, frontend/src/api/search.ts, search/search.go
Implemented streaming search responses with goroutines/channels and heartbeat; added context cancellation support; updated frontend search to use callbacks with AbortSignal
HTTP Handlers & Auth
http/auth.go, http/users.go, http/settings.go, http/public.go
Extended JWT token handling with renewal logic; added currentPassword validation for sensitive operations; added CalcImgRes option propagation; extended settingsData with new fields
File Operations
files/file.go, http/resource.go, http/tus_handlers.go
Added CalcImgRes flag for conditional image resolution calculation; added file.Sync() calls after writes; updated error references to use fberrors
User & Settings Types
users/users.go, settings/settings.go, settings/defaults.go
Added RedirectAfterCopyMove and AceEditorTheme fields to User/UserDefaults; added HideLoginButton, LogoutPage, HideDotfiles to Settings
Frontend Type Definitions
frontend/src/types/user.d.ts, frontend/src/types/settings.d.ts, frontend/src/types/layout.d.ts
Extended IUser/SettingsDefaults with new preference fields; added saveAction callback to PopupProps
Frontend Utilities & Constants
frontend/src/utils/auth.ts, frontend/src/utils/constants.ts, frontend/src/utils/theme.ts, frontend/src/api/utils.ts
Enhanced logout flow with reason parameter and logoutPage handling; added setSafeTimeout for large delays; added getEditorTheme for Ace editor theming
Frontend Components
frontend/src/components/prompts/Copy.vue, frontend/src/components/prompts/Move.vue, frontend/src/components/prompts/DiscardEditorChanges.vue, frontend/src/components/prompts/CreateFilePath.vue
Added conditional redirect logic based on redirectAfterCopyMove; new CreateFilePath component for path display; Save Changes button in editor prompts
Frontend File Editor
frontend/src/views/files/Editor.vue
Replaced hardcoded theme with dynamic getEditorTheme; removed wheel event listener; added saveAction in route update flow; added finishClose for unsaved changes handling
Image Processing
img/service.go, http/utils.go
Added ErrImageTooLarge with MaxImageWidth/MaxImageHeight constants; added dimension validation in detectFormat; extended error-to-status mapping
Frontend Package Upgrades
frontend/package.json, frontend/tsconfig.*.json, go.mod
Bumped Node >=24.0.0, PNPM >=10.0.0; updated frontend dependencies (Vue, Vite, tsconfig); Go 1.24→1.25 with dependency upgrades
Internationalization
frontend/src/i18n/*.json, frontend/src/i18n/index.ts
Added new locales (bg.json, hr.json, lv.json, lv_LV.json); added i18n keys for CSV, logout reasons, new settings; removed cs and nb dayjs locale loading
Frontend Test Files
frontend/playwright.config.ts, frontend/tests-examples/*, frontend/tests/auth.spec.ts, frontend/tests/fixtures/*, frontend/tests/settings.spec.ts
Removed Playwright configuration and test suite; deleted auth/settings test fixtures and demo tests
Documentation
www/docs/*.md, www/mkdocs.yml, CONTRIBUTING.md, README.md, CHANGELOG.md
Complete docs reorganization with new customization/authentication/command-execution pages; added 30+ CLI command docs; updated nav structure; updated project status text
Configuration & Manifests
commitlint.config.js, renovate.json, .gitignore, Dockerfile*, .golangci.yml, settings.json
Deleted commitlint config; added Renovate config; removed Playwright ignore patterns; updated Docker base images (3.22→3.23); streamlined golangci config; removed settings.json
Removed Legacy Code
cmd/upgrade.go, storage/bolt/importer/*, scripts/bump_version.sh, scripts/commitlint.sh, tools/*
Removed upgrade command; deleted importer functionality; removed version bump and commitlint scripts; cleaned up tools.mk and tools package
Auth & Storage
auth/hook.go, auth/proxy.go, storage/bolt/share.go, storage/bolt/users.go
Updated error alias references to fberrors; added RedirectAfterCopyMove support in hook auth; consolidated string operations (strings.ReplaceAll)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: merging upstream version 2.54.0 into the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
CONTRIBUTING.md (1)

102-102: Minor typo: double comma.

There's a double comma in the Translations section.

📝 Suggested fix
-Translations are managed on Transifex, which is an online website where everyone can contribute and translate strings for our project. It automatically syncs with the main language file \(in English\) and,, for you to contribute, you just need to:
+Translations are managed on Transifex, which is an online website where everyone can contribute and translate strings for our project. It automatically syncs with the main language file \(in English\) and, for you to contribute, you just need to:
files/file.go (1)

450-471: Duplicate image resolution calculation.

For image files, the resolution is calculated twice:

  1. Lines 450-457: Pre-calculates resolution based on MIME type
  2. Line 467: detectType is called with calcImgRes=true, which calculates resolution again at lines 256-262

This opens and decodes each image file twice, impacting performance for directories with many images.

Suggested fix: Remove pre-calculation, let detectType handle it
-		if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") && calcImgRes {
-			resolution, err := calculateImageResolution(file.Fs, file.Path)
-			if err != nil {
-				log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
-			} else {
-				file.Resolution = resolution
-			}
-		}
-
 		if file.IsDir {
 			listing.NumDirs++
 		} else {
frontend/src/i18n/index.ts (1)

61-93: Unreachable locale detection cases due to ordering.

The switch-case pattern matching has duplicate locale handlers where broader patterns precede narrower ones, making the latter unreachable:

  • /^ru.*/i (line 61) matches before /^ru\b/ (line 76)
  • /^tr.*/i (line 64) matches before /^tr\b/ (line 88)
  • /^uk.*/i (line 67) matches before /^uk\b/ (line 91)

Since the switch(true) evaluates cases top-to-bottom, the narrower patterns will never be reached. Either remove the duplicates or consolidate the logic.

Proposed fix - Remove duplicate cases
     case /^uk.*/i.test(locale):
       locale = "uk_UA";
       break;
-    case /^de\b/.test(locale):
-      locale = "de";
-      break;
-    case /^ro\b/.test(locale):
-      locale = "ro";
-      break;
-    case /^ru\b/.test(locale):
-      locale = "ru";
-      break;
-    case /^pl\b/.test(locale):
-      locale = "pl";
-      break;
-    case /^ko\b/.test(locale):
-      locale = "ko";
-      break;
-    case /^sk\b/.test(locale):
-      locale = "sk";
-      break;
-    case /^tr\b/.test(locale):
-      locale = "tr";
-      break;
-    case /^uk\b/.test(locale):
-      locale = "uk";
-      break;
-
+    case /^de\b/.test(locale):
+      locale = "de";
+      break;
+    case /^ro\b/.test(locale):
+      locale = "ro";
+      break;
+    case /^pl\b/.test(locale):
+      locale = "pl";
+      break;
+    case /^ko\b/.test(locale):
+      locale = "ko";
+      break;
+    case /^sk\b/.test(locale):
+      locale = "sk";
+      break;
     case /^vi\b/.test(locale):

Note: Keep only the first occurrence of ru, tr, and uk cases (lines 61-68) and remove the duplicate narrower patterns.

cmd/users_import.go (1)

54-71: Bug: Backup saves new users instead of existing users.

When replace is true, the intent is to back up existing users before deletion. However, line 60 marshals list (the new users being imported) instead of oldUsers (the existing users). This defeats the purpose of the backup.

Suggested fix
 		if replace {
 			oldUsers, userImportErr := st.Users.Gets("")
 			if userImportErr != nil {
 				return userImportErr
 			}

-			err = marshal("users.backup.json", list)
+			err = marshal("users.backup.json", oldUsers)
 			if err != nil {
 				return err
 			}
frontend/src/types/user.d.ts (1)

22-36: Add aceEditorTheme to IUserForm interface.

aceEditorTheme is editable via the user settings form (Profile.vue) and is submitted to the API as part of user profile updates, but is not defined in the IUserForm interface despite being present in IUser. Add aceEditorTheme?: string; to maintain type consistency.

frontend/src/views/settings/Profile.vue (1)

164-171: Validation may block password updates when currentPassword is not required.

When isCurrentPasswordRequired is false (non-JSON auth methods), currentPassword.value remains an empty string, causing the validation at line 167 to fail and block all password updates.

Consider making this check conditional:

🐛 Proposed fix
   if (
     password.value !== passwordConf.value ||
     password.value === "" ||
-    currentPassword.value === "" ||
+    (isCurrentPasswordRequired.value && currentPassword.value === "") ||
     authStore.user === null
   ) {
     return;
   }
cmd/users.go (1)

29-50: Header and data columns are misaligned.

The header on line 30 has 15 columns, but the fmt.Fprintf on line 33 outputs 16 values (the new RedirectAfterCopyMove field was added without updating the header). This will cause all columns after S.Click to be misaligned in the output.

Suggested fix - add missing header column
-	fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
+	fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tRedirect\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
cmd/root.go (1)

483-488: Typo and consider returning error instead of log.Fatal.

Line 483 has a typo: "initialize wth" should be "initialized with".

Also, using log.Fatal on line 487 prevents proper error propagation up the call stack. Consider returning an error instead to allow the caller to handle cleanup appropriately.

Suggested fix
 	} else {
-		log.Printf("User '%s' initialize wth user-provided password\n", username)
+		log.Printf("User '%s' initialized with user-provided password\n", username)
 	}

 	if username == "" || password == "" {
-		log.Fatal("username and password cannot be empty during quick setup")
+		return errors.New("username and password cannot be empty during quick setup")
 	}
🤖 Fix all issues with AI agents
In `@cmd/rule_rm.go`:
- Around line 56-64: The closures user and global perform unsafe slice removals
using u.Rules[:i] and u.Rules[f+1:] without validating i and f; add defensive
bounds checks at the start of each closure to ensure i and f are within 0 <= i
<= f < len(u.Rules) (and similarly for len(s.Rules)) and return a descriptive
error instead of proceeding if the indices are invalid; keep the calls to
st.Users.Save(u) and st.Settings.Save(s) unchanged but only call them after
validation.

In `@frontend/src/api/pub.ts`:
- Around line 43-45: The single-file branch appends files[0] raw to the URL,
which breaks for spaces/special chars; change it to append an encoded path
segment (e.g., use encodeURIComponent(files[0])) instead of files[0] so the URL
is properly escaped (keep the trailing "?" as before) — mirror the encoding
approach used in the multi-file branch that encodes arg.

In `@frontend/src/api/search.ts`:
- Around line 55-64: The loop in frontend/src/api/search.ts parses each line
into item and then checks item.isDir, but the backend sends the boolean as
item.dir; update the fallback handling so it uses the correct property (e.g.,
replace the check if (item.isDir) with if (item.dir) or normalize the object by
setting item.isDir = item.dir right after parsing) before appending "/" and
invoking callback(item).
- Around line 41-45: The code parses each line into a ResourceItem but checks
item.isDir while the backend sends "dir", so directory URLs never get a trailing
slash; update the parsing/logic in frontend/src/api/search.ts to use the backend
property (use item.dir) or explicitly map it (e.g., set item.isDir = item.dir
after JSON.parse) and then use that mapped value when appending the "/" to
item.url; ensure the ResourceItem type/signature is updated accordingly so
future checks refer to the correct property name (item.dir or item.isDir
consistently).

In `@frontend/src/components/prompts/UploadFiles.vue`:
- Around line 114-124: The throttling wrapper causes mismatched deltas because
calculateSpeed uses oldSentBytes from the watch while elapsedTime is measured
from lastSpeedUpdate; fix by introducing and maintaining a separate
lastSentBytes variable that is updated only when calculateSpeed actually runs:
add a module-level lastSentBytes, use it inside calculateSpeed to compute
bytesSinceLastCalc = sentBytes - lastSentBytes, update lastSentBytes and
lastSpeedUpdate at the end of calculateSpeed, and have throttledCalculateSpeed
call calculateSpeed(sentBytes) (or pass current sentBytes) instead of relying on
oldSentBytes so throttled calls compute correct speed.

In `@frontend/src/views/files/Editor.vue`:
- Around line 156-159: The saveAction currently calls save() then always calls
next(), so navigation proceeds even if the save fails; change saveAction to
await save(true) (to throw on failure) and only call next() when that await
succeeds, i.e. wrap the await save(true) in a try/catch or let the thrown error
prevent calling next(); reference saveAction, save(), and next() when updating
the callback so failed saves block navigation just like close() does.

In `@http/search.go`:
- Around line 25-57: The timer is created once (timeout := time.NewTimer(...))
but never reset so timeout.C only fires once; fix by resetting or replacing the
timer: either swap to a time.NewTicker and use ticker.C (stopping it on return),
or, if keeping the timer, call timeout.Reset(searchPingInterval * time.Second)
inside the timeout.C case (right after setting infoBytes = nil) so the heartbeat
continues each interval; keep the existing defer timeout.Stop() (or stop the
ticker) and leave the rest of the loop logic (response, ctx, cancel, w, flusher)
unchanged.

In `@www/docs/command-execution.md`:
- Around line 51-54: The Docker example is invalid because the base image "FROM
filebrowser/filebrowser" is BusyBox-based and lacks package managers; replace
that line by either building FROM a full distro (e.g., Ubuntu/Debian) and
install p7zip-full with apt and then install or copy the filebrowser binary, or
switch to the s6 variant "FROM filebrowser/filebrowser:s6" (Alpine-based) and
install p7zip via apk; update the document to show one of these two valid
approaches and remove the broken "RUN sudo apt install p7zip-full" example.
🟡 Minor comments (30)
frontend/test-results/.last-run.json-1-4 (1)

1-4: Add test-results directory to .gitignore.

The frontend/test-results/.last-run.json file is a test artifact that should not be committed to version control. The frontend project currently has no test framework configured (no test scripts in package.json and no testing dependencies), making this directory extraneous. Additionally, the file's "status": "failed" with an empty failedTests array suggests an incomplete or abandoned test run.

Add test-results/ to the root .gitignore to prevent future test artifacts from being committed.

www/docs/cli/filebrowser-config-import.md-7-12 (1)

7-12: Fix typos: "unexisting" and "nonexisting" should be "nonexistent".

The words "unexisting" (line 8) and "nonexisting" (line 10) are non-standard. The correct term is "nonexistent".

📝 Proposed fix
 Import a configuration file. This will replace all the existing
-configuration. Can be used with or without unexisting databases.
+configuration. It can be used with or without nonexistent databases.

-If used with a nonexisting database, a key will be generated
+If used with a nonexistent database, a key will be generated
 automatically. Otherwise the key will be kept the same as in the
 database.
frontend/src/css/context-menu.css-11-17 (1)

11-17: Duplicate display property will cause unexpected behavior.

Line 12 sets display: block which is immediately overridden by display: flex on line 15. The first declaration is redundant and should be removed.

Proposed fix
 .context-menu .action {
-  display: block;
   width: 100%;
   border-radius: 0;
   display: flex;
   align-items: center;
 }
frontend/src/stores/auth.ts-10-14 (1)

10-14: Clear the logout timer in clearUser() to prevent stale timeout execution.

The logoutTimer state stores a timeout handle, but when clearUser() calls this.$reset() during logout, it only resets the state reference without canceling the pending timeout. The clearTimeout() call exists in parseToken() to clear old timers before setting new ones, but it's missing during logout. If a timeout is scheduled and logout is triggered, the timeout will still fire after the user is cleared, potentially causing unexpected behavior.

♻️ Suggested fix
     // easily reset state using `$reset`
     clearUser() {
+      if (this.logoutTimer !== null) {
+        clearTimeout(this.logoutTimer);
+      }
       this.$reset();
     },
frontend/src/i18n/hr.json-46-46 (1)

46-46: Untranslated strings in Croatian localization file.

Several entries remain in English and should be translated to Croatian:

  • Line 46: "stopSearch": "Stop searching" → should be Croatian (e.g., "Zaustavi pretraživanje")
  • Line 175: "aceEditorTheme": "Ace editor theme" → should be Croatian (e.g., "Tema Ace uređivača")
  • Line 262: "currentPassword": "Your Current Password" → should be Croatian (e.g., "Vaša trenutna lozinka")

Also applies to: 175-175, 262-262

frontend/src/api/utils.ts-95-111 (1)

95-111: Potential issue: returned timeout ID cannot cancel chained timeouts.

The function returns the initial timeout ID, but when remaining > MAX_DELAY, subsequent scheduled timeouts receive new IDs. If a caller uses clearTimeout with the returned ID after the first chunk has elapsed, the remaining chain will continue executing.

If cancellation is a requirement for the logout timer use case, consider returning a cancel function or tracking the latest timeout ID:

💡 Suggested fix with cancellation support
-export function setSafeTimeout(callback: () => void, delay: number): number {
+export function setSafeTimeout(callback: () => void, delay: number): () => void {
   const MAX_DELAY = 86_400_000;
   let remaining = delay;
+  let timeoutId: number;

   function scheduleNext(): number {
     if (remaining <= MAX_DELAY) {
-      return window.setTimeout(callback, remaining);
+      timeoutId = window.setTimeout(callback, remaining);
+      return timeoutId;
     } else {
-      return window.setTimeout(() => {
+      timeoutId = window.setTimeout(() => {
         remaining -= MAX_DELAY;
         scheduleNext();
       }, MAX_DELAY);
+      return timeoutId;
     }
   }

-  return scheduleNext();
+  scheduleNext();
+  return () => window.clearTimeout(timeoutId);
 }
frontend/src/i18n/bg.json-46-50 (1)

46-50: Several strings remain untranslated.

The following keys are still in English and should be translated to Bulgarian for consistency:

  • buttons.stopSearch, buttons.editAsText, buttons.increaseFontSize, buttons.decreaseFontSize
  • files.csvTooLarge, files.csvLoadFailed, files.showingRows, files.columnSeparator, files.csvSeparators.*
  • login.passwordTooShort
  • settings.currentPassword

This can be addressed in a follow-up if translations are pending.

Also applies to: 83-91, 118-118, 262-262

frontend/src/components/settings/AceEditorTheme.vue-25-27 (1)

25-27: Incorrect type cast for event target.

SelectHTMLAttributes is a Vue type for component props, not for DOM element access. The cast should use HTMLSelectElement to properly type the event target.

Suggested fix
-import { type SelectHTMLAttributes } from "vue";
 import { themes } from "ace-builds/src-noconflict/ext-themelist";
 
 // ...
 
 const change = (event: Event) => {
-  emit("update:aceEditorTheme", (event.target as SelectHTMLAttributes)?.value);
+  emit("update:aceEditorTheme", (event.target as HTMLSelectElement)?.value);
 };
frontend/src/i18n/lv.json-46-46 (1)

46-46: Untranslated strings detected.

Several strings remain in English and should be translated to Latvian:

  • Line 46: "stopSearch": "Stop searching" → should be "Pārtraukt meklēšanu"
  • Line 177: "administrator": "Administrator" → should be "Administrators"
  • Line 262: "currentPassword": "Your Current Password" → should be "Jūsu pašreizējā parole"

Also applies to: 177-177, 262-262

frontend/src/components/ContextMenu.vue-22-27 (1)

22-27: Computed left may use stale clientWidth on initial render.

When show transitions to true, this computed runs before the element is actually rendered, so contextMenu.value?.clientWidth will likely be 0 or undefined. The menu position won't account for its actual width on the first render.

Consider using nextTick or a ResizeObserver to recalculate position after the element is visible.

Suggested approach using nextTick
+import { ref, watch, computed, onUnmounted, nextTick } from "vue";
+
+const left = ref(0);
+
+const calculateLeft = () => {
+  nextTick(() => {
+    left.value = Math.min(
+      props.pos.x,
+      window.innerWidth - (contextMenu.value?.clientWidth ?? 0)
+    );
+  });
+};
+
+watch(
+  () => [props.show, props.pos.x],
+  ([show]) => {
+    if (show) {
+      calculateLeft();
+    }
+  },
+  { immediate: true }
+);
frontend/src/utils/auth.ts-31-37 (1)

31-37: Guard against missing or expired exp claim.

The non-null assertion on data.exp! could cause issues if the JWT lacks an exp claim (resulting in NaN timeout). Additionally, an already-expired token would produce a negative timeout.

Suggested fix
-  const expiresAt = new Date(data.exp! * 1000);
-  const timeout = expiresAt.getTime() - Date.now();
+  if (!data.exp) {
+    console.warn("JWT missing exp claim, idle timeout disabled");
+    return;
+  }
+  const expiresAt = new Date(data.exp * 1000);
+  const timeout = expiresAt.getTime() - Date.now();
+  if (timeout <= 0) {
+    logout("expired");
+    return;
+  }
frontend/src/i18n/ru_RU.json-284-284 (1)

284-284: Untranslated string in Russian locale.

The currentPassword value is in English ("Your Current Password") but should be translated to Russian.

Suggested fix
-    "currentPassword": "Your Current Password"
+    "currentPassword": "Ваш текущий пароль"
frontend/src/i18n/ru_RU.json-50-50 (1)

50-50: Untranslated string in Russian locale.

The stopSearch value is in English ("Stop searching") but should be translated to Russian for consistency with the rest of the locale file.

Suggested fix
-    "stopSearch": "Stop searching",
+    "stopSearch": "Остановить поиск",
www/docs/cli/filebrowser-config-init.md-8-11 (1)

8-11: Minor grammar issue: hyphenate "user-related".

When used as a compound adjective before a noun, "user related" should be hyphenated.

📝 Suggested fix
-to the defaults when creating new users and you don't
+to the defaults when creating new users and you don't

Change line 9 from:

'filebrowser config set'. The user related flags apply

to:

'filebrowser config set'. The user-related flags apply
www/docs/authentication.md-43-43 (1)

43-43: Incorrect heading level for "No Authentication" section.

"No Authentication" is described as the third authentication method (alongside JSON Auth and Proxy Header), but it's rendered as a subsection (###) under "Proxy Header" instead of a top-level section (##).

Proposed fix
-### No Authentication
+## No Authentication
cmd/config.go-316-319 (1)

316-319: Error from GetString is silently ignored when it fails.

If flags.GetString(flag.Name) returns an error, the error is only used in the if condition but not appended to the errs slice. The err variable inside the if block shadows the outer err, so a failure here goes unreported.

Proposed fix
 		case "hidden-files":
-			if hiddenFileString, err := flags.GetString(flag.Name); err == nil {
+			hiddenFileString, err := flags.GetString(flag.Name)
+			if err == nil {
 				ser.HiddenFiles = convertFileStrToFileMap(hiddenFileString)
 			}
.github/workflows/docs.yml-5-7 (1)

5-7: Path patterns may not trigger as expected.

The path 'www' only matches a file named www, not files within the www/ directory. Use 'www/**' to match all files recursively.

Suggested fix
   pull_request:
     paths:
-      - 'www'
+      - 'www/**'
       - '*.md'
.github/workflows/docs.yml-51-52 (1)

51-52: Missing step id causes undefined steps.deployment reference.

Line 35 references steps.deployment.outputs.page_url, but the deploy step lacks an id: deployment attribute. The environment URL won't be set correctly.

Suggested fix
       - name: Deploy to GitHub Pages
+        id: deployment
         uses: actions/deploy-pages@v4
frontend/src/i18n/lv_LV.json-46-46 (1)

46-46: Untranslated strings remain in English.

Two strings are still in English and should be translated to Latvian for consistency:

  • Line 46: "stopSearch": "Stop searching" → should be Latvian (e.g., "Apturēt meklēšanu")
  • Line 262: "currentPassword": "Your Current Password" → should be Latvian (e.g., "Jūsu pašreizējā parole")

Also applies to: 262-262

frontend/src/views/files/FileListing.vue-1155-1163 (1)

1155-1163: target.closest("item") will never match - remove redundant check.

target.closest("item") looks for an HTML element with tag name <item>, which doesn't exist in the DOM (the Vue <item> component renders as a <div class="item">). This check is redundant since line 1159 already checks target.closest(".item").

🔧 Suggested fix
 const handleEmptyAreaClick = (e: MouseEvent) => {
   const target = e.target;
   if (!(target instanceof HTMLElement)) return;

-  if (target.closest("item") || target.closest(".item")) return;
+  if (target.closest(".item")) return;

   if (target.closest(".context-menu")) return;

   fileStore.selected = [];
 };
cmd/users_update.go-54-65 (1)

54-65: Missing QuotaFile field in defaults initialization.

The defaults struct is missing the QuotaFile field which exists in both settings.UserDefaults and users.User. This means if a user has a custom QuotaFile set, it won't be preserved through the update flow via getUserDefaults.

Proposed fix
 defaults := settings.UserDefaults{
 	Scope:                 user.Scope,
 	TmpDir:                user.TmpDir,
 	TrashDir:              user.TrashDir,
+	QuotaFile:             user.QuotaFile,
 	Locale:                user.Locale,
 	ViewMode:              user.ViewMode,
 	SingleClick:           user.SingleClick,
 	RedirectAfterCopyMove: user.RedirectAfterCopyMove,
 	Perm:                  user.Perm,
 	Sorting:               user.Sorting,
 	Commands:              user.Commands,
 }
frontend/src/i18n/es_CO.json-50-55 (1)

50-55: Missing Spanish translations for new keys.

Several newly added keys are left in English instead of being translated to Spanish. This creates an inconsistent user experience for Spanish-speaking users.

Untranslated keys include:

  • buttons.stopSearch, buttons.editAsText, buttons.increaseFontSize, buttons.decreaseFontSize
  • All CSV-related strings under files
  • login.passwordTooShort, login.logout_reasons.inactivity
  • settings.aceEditorTheme, settings.hideLoginButton, settings.currentPassword

Would you like me to open an issue to track the missing Spanish translations?

Also applies to: 95-103, 130-133, 200-200, 208-208, 287-287

cmd/utils.go-121-129 (1)

121-129: Config parse error check uses incorrect type comparison.

The errors.Is(err, viper.ConfigParseError{}) check will not work as intended because errors.Is compares error values, not types. A zero-value ConfigParseError{} won't match actual parse errors.

Suggested fix
 	// Read in configuration
 	if err := v.ReadInConfig(); err != nil {
-		if errors.Is(err, viper.ConfigParseError{}) {
+		var parseErr viper.ConfigParseError
+		if errors.As(err, &parseErr) {
 			return nil, err
 		}
 
 		log.Println("No config file used")
frontend/src/i18n/zh_CN.json-50-55 (1)

50-55: Incomplete translation: stopSearch remains in English.

The key stopSearch has the value "Stop searching" which should be translated to Chinese for consistency with the rest of the file. Consider: "停止搜索"

frontend/src/i18n/zh_CN.json-296-297 (1)

296-297: Incomplete translation: currentPassword remains in English.

The key currentPassword has the value "Your Current Password" which should be translated to Chinese. Consider: "您当前的密码"

frontend/src/i18n/pt_PT.json-87-96 (1)

87-96: CSV-related strings untranslated.

All new CSV-related strings (noPreview, csvTooLarge, csvLoadFailed, showingRows, columnSeparator, csvSeparators.*) remain in English. Consider translating these to Portuguese.

frontend/src/i18n/pt_PT.json-122-126 (1)

122-126: Login-related strings untranslated.

The new keys passwordTooShort and logout_reasons.inactivity are in English. These user-facing messages should be translated to Portuguese.

frontend/src/i18n/pt_PT.json-193-193 (1)

193-193: Settings strings partially untranslated.

aceEditorTheme, hideLoginButton, and currentPassword remain in English while nearby additions like brandingDirectoryPath and brandingHelp are translated. Consider completing the Portuguese translations for consistency.

Also applies to: 202-202, 279-280

frontend/src/i18n/pt_PT.json-49-54 (1)

49-54: Multiple untranslated strings in buttons section.

The new button keys (discardChanges, stopSearch, saveChanges, editAsText, increaseFontSize, decreaseFontSize) are all in English. These should be translated to Portuguese for consistency with the existing translations in this file.

cmd/root.go-318-324 (1)

318-324: Variable shadowing: v shadows the function parameter.

On line 321, the variable v in the else if v := os.Getenv("FB_BASEURL") shadows the function parameter v *viper.Viper. While this works correctly because the Viper parameter isn't used within this block, it reduces code clarity and could lead to bugs during future modifications.

Suggested fix
 	if v.IsSet("baseURL") {
 		server.BaseURL = v.GetString("baseURL")
 		// TODO(remove): remove after July 2026.
-	} else if v := os.Getenv("FB_BASEURL"); v != "" {
+	} else if baseURLEnv := os.Getenv("FB_BASEURL"); baseURLEnv != "" {
 		log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
-		server.BaseURL = v
+		server.BaseURL = baseURLEnv
 	}

Comment thread cmd/rule_rm.go
Comment on lines 56 to 64
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
return d.store.Users.Save(u)
return st.Users.Save(u)
}

global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
return d.store.Settings.Save(s)
return st.Settings.Save(s)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the full context of cmd/rule_rm.go around lines 56-64
head -80 cmd/rule_rm.go | tail -40

Repository: hostinger/filebrowser

Length of output: 922


🏁 Script executed:

#!/bin/bash
# Let's see the entire file to understand the structure and where indices come from
wc -l cmd/rule_rm.go

Repository: hostinger/filebrowser

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Get the full file content to trace the logic
cat -n cmd/rule_rm.go

Repository: hostinger/filebrowser

Length of output: 2197


🏁 Script executed:

#!/bin/bash
# Search for runRules function definition
ast-grep --pattern 'func runRules($_) {
  $$$
}'

Repository: hostinger/filebrowser

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Search for runRules function definition
rg "func runRules" --type go

Repository: hostinger/filebrowser

Length of output: 214


🏁 Script executed:

#!/bin/bash
# Search where runRules is defined and how it uses the callbacks
rg "runRules" -A 20 --type go

Repository: hostinger/filebrowser

Length of output: 1157


🏁 Script executed:

#!/bin/bash
# Get the complete runRules function from cmd/rules.go
rg "func runRules" -A 40 --type go cmd/rules.go

Repository: hostinger/filebrowser

Length of output: 808


🏁 Script executed:

#!/bin/bash
# Let's also check the entire rules.go file to understand the context better
cat -n cmd/rules.go | head -100

Repository: hostinger/filebrowser

Length of output: 2851


🏁 Script executed:

#!/bin/bash
# Verify that strconv.Atoi can indeed parse negative numbers
python3 << 'EOF'
import subprocess
result = subprocess.run(['go', 'run', '-'], input=b'''package main
import (
	"fmt"
	"strconv"
)
func main() {
	i, _ := strconv.Atoi("-5")
	fmt.Println("Parsed:", i)
}
''', capture_output=True, cwd='/tmp')
print(result.stdout.decode() if result.stdout else result.stderr.decode())
EOF

Repository: hostinger/filebrowser

Length of output: 189


🏁 Script executed:

#!/bin/bash
# Check if there's any validation of indices elsewhere before the closures are called
rg "len.*Rules" --type go cmd/ | head -20

Repository: hostinger/filebrowser

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Check rules_add.go to see if it does bounds validation for comparison
cat -n cmd/rules_add.go

Repository: hostinger/filebrowser

Length of output: 1888


Add bounds validation before slice operations to prevent panics.

The slice operations u.Rules[:i] and u.Rules[f+1:] will panic if indices exceed slice bounds or are negative. Since i and f are parsed directly from user input via strconv.Atoi() with no validation, callers can trigger panics by passing invalid indices like -1 or values exceeding the Rules length. Add defensive bounds checks in the user and global closures before performing the slice operations.

🤖 Prompt for AI Agents
In `@cmd/rule_rm.go` around lines 56 - 64, The closures user and global perform
unsafe slice removals using u.Rules[:i] and u.Rules[f+1:] without validating i
and f; add defensive bounds checks at the start of each closure to ensure i and
f are within 0 <= i <= f < len(u.Rules) (and similarly for len(s.Rules)) and
return a descriptive error instead of proceeding if the indices are invalid;
keep the calls to st.Users.Save(u) and st.Settings.Save(s) unchanged but only
call them after validation.

Comment thread frontend/src/api/pub.ts
Comment on lines 43 to 45
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
url += files[0] + "?";
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing URL encoding for single-file path.

The single-file case appends files[0] directly to the URL path without encoding. If the filename contains special characters (spaces, #, ?, &, unicode, etc.), the resulting URL will be malformed or incorrect.

In contrast, the multi-file case (lines 48-54) correctly encodes the arg string before appending it as a query parameter.

🐛 Proposed fix
  if (files.length === 1) {
-   url += files[0] + "?";
+   url += encodeURIComponent(files[0]) + "?";
  } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
url += files[0] + "?";
} else {
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
🤖 Prompt for AI Agents
In `@frontend/src/api/pub.ts` around lines 43 - 45, The single-file branch appends
files[0] raw to the URL, which breaks for spaces/special chars; change it to
append an encoded path segment (e.g., use encodeURIComponent(files[0])) instead
of files[0] so the URL is properly escaped (keep the trailing "?" as before) —
mirror the encoding approach used in the multi-file branch that encodes arg.

Comment on lines +41 to +45
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
item.url += "/";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Property name mismatch: backend sends dir, but code checks isDir.

Looking at the backend in http/search.go (line 65), the response sends "dir": f.IsDir(). However, this code checks item.isDir. This will cause the trailing slash to never be appended to directory URLs.

Proposed fix
         const item = JSON.parse(line) as ResourceItem;
         item.url = `/files${base}` + url.encodePath(item.path);
-        if (item.isDir) {
+        if (item.dir) {
           item.url += "/";
         }
+        item.isDir = item.dir;
         callback(item);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
item.url += "/";
}
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
item.isDir = item.dir;
🤖 Prompt for AI Agents
In `@frontend/src/api/search.ts` around lines 41 - 45, The code parses each line
into a ResourceItem but checks item.isDir while the backend sends "dir", so
directory URLs never get a trailing slash; update the parsing/logic in
frontend/src/api/search.ts to use the backend property (use item.dir) or
explicitly map it (e.g., set item.isDir = item.dir after JSON.parse) and then
use that mapped value when appending the "/" to item.url; ensure the
ResourceItem type/signature is updated accordingly so future checks refer to the
correct property name (item.dir or item.isDir consistently).

Comment on lines +55 to +64
for (const line of lines) {
if (line) {
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
item.url += "/";
}
callback(item);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same property name mismatch in fallback path.

The fallback path has the same issue - checking item.isDir when the backend sends dir.

Proposed fix
       const lines = text.split(/\n/);
       for (const line of lines) {
         if (line) {
           const item = JSON.parse(line) as ResourceItem;
           item.url = `/files${base}` + url.encodePath(item.path);
-          if (item.isDir) {
+          if (item.dir) {
             item.url += "/";
           }
+          item.isDir = item.dir;
           callback(item);
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const line of lines) {
if (line) {
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
item.url += "/";
}
callback(item);
}
}
for (const line of lines) {
if (line) {
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
item.isDir = item.dir;
callback(item);
}
}
🤖 Prompt for AI Agents
In `@frontend/src/api/search.ts` around lines 55 - 64, The loop in
frontend/src/api/search.ts parses each line into item and then checks
item.isDir, but the backend sends the boolean as item.dir; update the fallback
handling so it uses the correct property (e.g., replace the check if
(item.isDir) with if (item.dir) or normalize the object by setting item.isDir =
item.dir right after parsing) before appending "/" and invoking callback(item).

Comment on lines +114 to +124
let lastThrottleTime = 0;

const throttledCalculateSpeed = (sentBytes: number, oldSentBytes: number) => {
const now = Date.now();
if (now - lastThrottleTime < 100) {
return;
}

lastThrottleTime = now;
calculateSpeed(sentBytes, oldSentBytes);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Speed calculation is inaccurate when throttling skips updates.

The throttled wrapper skips calculateSpeed calls, but calculateSpeed uses oldSentBytes from the watch callback (value from previous watch trigger) while elapsedTime is computed from lastSpeedUpdate (timestamp from last calculation). When updates are throttled, these values become mismatched:

  • elapsedTime = time since last calculation
  • bytesSinceLastUpdate = bytes since last watch trigger

This causes under-reported speeds when throttling is active.

🔧 Proposed fix: Track lastSentBytes separately
 let lastSpeedUpdate: number = 0;
 let recentSpeeds: number[] = [];
+let lastSentBytesAtCalculation: number = 0;

 let lastThrottleTime = 0;

-const throttledCalculateSpeed = (sentBytes: number, oldSentBytes: number) => {
+const throttledCalculateSpeed = (sentBytes: number) => {
   const now = Date.now();
   if (now - lastThrottleTime < 100) {
     return;
   }

   lastThrottleTime = now;
-  calculateSpeed(sentBytes, oldSentBytes);
+  calculateSpeed(sentBytes);
 };

-const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
+const calculateSpeed = (sentBytes: number) => {
   // Reset the state when the uploads batch is complete
   if (sentBytes === 0) {
     lastSpeedUpdate = 0;
     recentSpeeds = [];
+    lastSentBytesAtCalculation = 0;

     eta.value = Infinity;
     speed.value = 0;

     return;
   }

   const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
-  const bytesSinceLastUpdate = sentBytes - oldSentBytes;
+  const bytesSinceLastUpdate = sentBytes - lastSentBytesAtCalculation;
   const currentSpeed = bytesSinceLastUpdate / elapsedTime;

   recentSpeeds.push(currentSpeed);
   if (recentSpeeds.length > 5) {
     recentSpeeds.shift();
   }

   const recentSpeedsAverage =
     recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;

   // Use the current speed for the first update to avoid smoothing lag
   if (recentSpeeds.length === 1) {
     speed.value = currentSpeed;
   }

   speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;

   lastSpeedUpdate = Date.now();
+  lastSentBytesAtCalculation = sentBytes;

   calculateEta();
 };
🤖 Prompt for AI Agents
In `@frontend/src/components/prompts/UploadFiles.vue` around lines 114 - 124, The
throttling wrapper causes mismatched deltas because calculateSpeed uses
oldSentBytes from the watch while elapsedTime is measured from lastSpeedUpdate;
fix by introducing and maintaining a separate lastSentBytes variable that is
updated only when calculateSpeed actually runs: add a module-level
lastSentBytes, use it inside calculateSpeed to compute bytesSinceLastCalc =
sentBytes - lastSentBytes, update lastSentBytes and lastSpeedUpdate at the end
of calculateSpeed, and have throttledCalculateSpeed call
calculateSpeed(sentBytes) (or pass current sentBytes) instead of relying on
oldSentBytes so throttled calls compute correct speed.

Comment on lines +156 to +159
saveAction: async () => {
await save();
next();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Navigation proceeds even if save fails.

In the saveAction callback, save() is called without throwError, so if the save operation fails, next() will still be executed, navigating away despite the failed save. This differs from the close() logic (lines 226-230) which properly uses save(true) and catches errors to prevent navigation.

Proposed fix
     saveAction: async () => {
-      await save();
-      next();
+      try {
+        await save(true);
+        next();
+      } catch {}
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
saveAction: async () => {
await save();
next();
},
saveAction: async () => {
try {
await save(true);
next();
} catch {}
},
🤖 Prompt for AI Agents
In `@frontend/src/views/files/Editor.vue` around lines 156 - 159, The saveAction
currently calls save() then always calls next(), so navigation proceeds even if
the save fails; change saveAction to await save(true) (to throw on failure) and
only call next() when that await succeeds, i.e. wrap the await save(true) in a
try/catch or let the thrown error prevent calling next(); reference saveAction,
save(), and next() when updating the callback so failed saves block navigation
just like close() does.

Comment thread http/commands.go
}

cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f "commands.go"

Repository: hostinger/filebrowser

Length of output: 100


🏁 Script executed:

git ls-files | grep -E "(commands|runner)" | head -20

Repository: hostinger/filebrowser

Length of output: 187


🏁 Script executed:

cat -n http/commands.go | head -100

Repository: hostinger/filebrowser

Length of output: 2960


🏁 Script executed:

cat -n runner/parser.go

Repository: hostinger/filebrowser

Length of output: 871


🏁 Script executed:

cat -n runner/commands.go | head -150

Repository: hostinger/filebrowser

Length of output: 4428


Remove nolint:gosec exemption or add input sanitization when Shell mode is enabled.

The safeguards mentioned are incomplete. While ParseCommand correctly parses the command name (validated against whitelist), it does not sanitize input. When Shell is configured in settings, the entire unparsed raw string is passed directly to the shell (runner/parser.go, lines 20–21), bypassing the whitelist protection. A user with an allowed command can inject shell metacharacters to execute arbitrary commands.

For example, a user with whoami whitelisted could input whoami; rm -rf / and have both commands executed when Shell mode is enabled.

Mitigation options:

  • Restore nolint:gosec only if Shell is disabled or definitively empty in your deployment
  • Implement input sanitization to escape shell metacharacters when Shell mode is active
  • Enforce no-shell mode by disallowing Shell configuration

Comment thread http/search.go
Comment on lines +25 to +57
timeout := time.NewTimer(searchPingInterval * time.Second)
defer timeout.Stop()
for {
var err error
var infoBytes []byte
select {
case info := <-response:
if info == nil {
return
}
infoBytes, err = json.Marshal(info)
case <-timeout.C:
// Send a heartbeat packet
infoBytes = nil
case <-ctx.Done():
return
}
if err != nil {
cancel(err)
return
}
_, err = w.Write(infoBytes)
if err == nil {
_, err = w.Write([]byte("\n"))
}
if err != nil {
cancel(err)
return
}
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Timer not reset after heartbeat, causing only one heartbeat to be sent.

The timer is created once but never reset after firing. After the first heartbeat at 5 seconds, timeout.C will never fire again because time.Timer channels only receive one value unless explicitly reset with Reset().

Proposed fix
 go func() {
   defer wg.Done()
   // Avoid connection timeout
   timeout := time.NewTimer(searchPingInterval * time.Second)
   defer timeout.Stop()
   for {
     var err error
     var infoBytes []byte
     select {
     case info := <-response:
       if info == nil {
         return
       }
       infoBytes, err = json.Marshal(info)
+      if !timeout.Stop() {
+        select {
+        case <-timeout.C:
+        default:
+        }
+      }
+      timeout.Reset(searchPingInterval * time.Second)
     case <-timeout.C:
       // Send a heartbeat packet
       infoBytes = nil
+      timeout.Reset(searchPingInterval * time.Second)
     case <-ctx.Done():
       return
     }
🤖 Prompt for AI Agents
In `@http/search.go` around lines 25 - 57, The timer is created once (timeout :=
time.NewTimer(...)) but never reset so timeout.C only fires once; fix by
resetting or replacing the timer: either swap to a time.NewTicker and use
ticker.C (stopping it on return), or, if keeping the timer, call
timeout.Reset(searchPingInterval * time.Second) inside the timeout.C case (right
after setting infoBytes = nil) so the heartbeat continues each interval; keep
the existing defer timeout.Stop() (or stop the ticker) and leave the rest of the
loop logic (response, ctx, cancel, w, flusher) unchanged.

Comment on lines +51 to +54
> ```docker
> FROM filebrowser/filebrowser
> RUN sudo apt install p7zip-full
> ```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

filebrowser/filebrowser docker image base OS alpine debian

💡 Result:

The official filebrowser/filebrowser Docker image is Alpine-based (there’s a plain Alpine image plus an s6-overlay variant). It is not Debian-based by default. [1][2][3]

Sources:

  • File Browser installation docs (Alpine + s6-overlay images). [1]
  • Official filebrowser/filebrowser Docker Hub listing (image tags including s6 variant). [2]
  • Example image scan / metadata showing Alpine base. [3]

🏁 Script executed:

#!/bin/bash
# Search for filebrowser Dockerfile in common locations
fd -e Dockerfile | head -20
# Also try to find any info about filebrowser in repo
rg -i "filebrowser" -t md | head -20

Repository: hostinger/filebrowser

Length of output: 3317


🏁 Script executed:

#!/bin/bash
# Check if there's information about the image in the current repo
git ls-files | grep -i docker

Repository: hostinger/filebrowser

Length of output: 315


🏁 Script executed:

#!/bin/bash
# Read the main Dockerfile to check base image and structure
head -30 Dockerfile

Repository: hostinger/filebrowser

Length of output: 1234


🏁 Script executed:

#!/bin/bash
# Check the Dockerfile.s6 as well
head -30 Dockerfile.s6

Repository: hostinger/filebrowser

Length of output: 729


🏁 Script executed:

#!/bin/bash
# Also check the www/Dockerfile since it might be related to documentation
cat www/Dockerfile

Repository: hostinger/filebrowser

Length of output: 187


🏁 Script executed:

#!/bin/bash
# Read the full command-execution.md to understand the context
cat -n www/docs/command-execution.md

Repository: hostinger/filebrowser

Length of output: 2981


Docker example is fundamentally broken and cannot be fixed with suggested apk command.

The filebrowser/filebrowser base image uses BusyBox (minimal, no package managers). The suggested apk add --no-cache p7zip won't work either since BusyBox doesn't include apk.

To extend filebrowser with additional tools like 7z, build from a full OS image instead:

-FROM filebrowser/filebrowser
-RUN sudo apt install p7zip-full
+FROM alpine:latest
+RUN apk add --no-cache p7zip
+COPY --from=filebrowser/filebrowser:latest / /

Alternatively, use the s6-overlay variant as a base (e.g., FROM filebrowser/filebrowser:s6), which includes Alpine and package managers.

🤖 Prompt for AI Agents
In `@www/docs/command-execution.md` around lines 51 - 54, The Docker example is
invalid because the base image "FROM filebrowser/filebrowser" is BusyBox-based
and lacks package managers; replace that line by either building FROM a full
distro (e.g., Ubuntu/Debian) and install p7zip-full with apt and then install or
copy the filebrowser binary, or switch to the s6 variant "FROM
filebrowser/filebrowser:s6" (Alpine-based) and install p7zip via apk; update the
document to show one of these two valid approaches and remove the broken "RUN
sudo apt install p7zip-full" example.

@laurynasgadl laurynasgadl merged commit 4f5c0f5 into master Jan 14, 2026
8 checks passed
@laurynasgadl laurynasgadl deleted the chore/merge-upstream-2.54.0 branch January 14, 2026 07:58
laurynasgadl added a commit that referenced this pull request Jan 14, 2026
@laurynasgadl laurynasgadl restored the chore/merge-upstream-2.54.0 branch January 14, 2026 08:06
@laurynasgadl laurynasgadl deleted the chore/merge-upstream-2.54.0 branch January 14, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.