Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).

## v0.8.4-beta

**New Features**:
- New media player styles and features
- Custom Media Player: enhanced media player using plyr thanks @Kurami32 (see #1160)
- Custom Media Player: also adds support for metadata
- added embeded video subtitle support (for both native and custom player). @maxbin123 #1072 #1157
- Users can disable the customer player and opt of native in profile settings.
- Option to disable backend update check via `server.disableUpdateCheck` #1134
- added `frontend.favicon` and `frontend.description` for html overrides
- onlyoffice is now supported in shares. Both viewing and editing can be configured per-share.
- Added only office debug view and wiki to assist with debugging issues #1068 #911 #1074
- Dark mode enforcement possible for shared links #1029
- added `System & Admin` section to settings
- includes a new config viewer to see current running config (hides secrets) #838
- added `server.minSearchLength` to allow adjusting the length requirement for search #1174

**Notes**:
- access management: specific folders/files with access are shown instead permission denied for parent folder
- navigation no longer appends last location hash which should fix some unwanted navation behavior #1070
- altered the context menu style and behavior.
- documentation update: comma or Space separated extensions #1138
- Files and folders can be created with "/" or "\" on the name #1126
- Share management should not be allowed without authentication #1163
- Question about customizing session timeout #1184

**BugFixes**:
- access management: delay showing rule changes in the list fixed. #1131
- Color names are not localized #1159
- rename issues #1170 #1171
- some shortcuts not working #1056
- Can't copy/paste text on mobile #1168
- Can't change between images inside of the share image viewer. #1144
- fixed and updated translations with variables always showing english.

## v0.8.3-beta

**BugFixes**:
Expand All @@ -18,7 +53,7 @@ All notable changes to this project will be documented in this file. For commit
**Notes**:
- 8.0 ffmpeg version bundled with docker
- go 1.25 upgrade with green tea GC enabled
- totp secrets accept non-secure strings, only throwing warning
- totp secrets accept non-secure strings, only throwing warning
- adjusted download limit so it also counts viewing text "content" of files (like in editor). You can also "disable file viewing" to stop the editor from showing. lower quality file image previews are not counted as downloads.
- updated invalid share message to be more clear https://github.com/gtsteffaniak/filebrowser/issues/1120

Expand Down
3 changes: 3 additions & 0 deletions _docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "[email protected]:" \
&& git config --global url."https://github.com/".insteadOf "ssh://[email protected]/"
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
Expand Down
3 changes: 3 additions & 0 deletions _docker/Dockerfile.slim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "[email protected]:" \
&& git config --global url."https://github.com/".insteadOf "ssh://[email protected]/"
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
Expand Down
2 changes: 1 addition & 1 deletion _docker/src/proxy/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
globalSetup: "./tests/playwright/proxy-setup.ts",
timeout: 3000,
timeout: 5000,
testDir: "./tests/playwright/proxy",
/* Run tests in files in parallel */
fullyParallel: false,
Expand Down
80 changes: 58 additions & 22 deletions backend/adapters/fs/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)

func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) {
response := iteminfo.ExtendedFileInfo{}
func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, error) {
response := &iteminfo.ExtendedFileInfo{}
index := indexing.GetIndex(opts.Source)
if index == nil {
return response, fmt.Errorf("could not get index: %v ", opts.Source)
}
if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) {
return response, errors.ErrPermissionDenied
}

realPath, isDir, err := index.GetRealPath(opts.Path)
if err != nil {
return response, err
return response, fmt.Errorf("could not get real path for requested path: %v", opts.Path)
}
opts.IsDir = isDir
var info *iteminfo.FileInfo
Expand All @@ -60,31 +58,69 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
}
}
if opts.Content {
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
content, err := getContent(realPath)
if err != nil {
logger.Debugf("could not get content for file: "+info.Path, info.Name, err)
return response, err
response.FileInfo = *info
response.RealPath = realPath
response.Source = opts.Source

if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) {
// check if any subpath is permitted
// keep track of permitted paths and only show them at the end
subFolders := info.Folders
subFiles := info.Files
response.Folders = make([]iteminfo.ItemInfo, 0)
response.Files = make([]iteminfo.ItemInfo, 0)
hasPermittedPaths := false
for _, subFolder := range subFolders {
indexPath := info.Path + subFolder.Name
if opts.Access.Permitted(index.Path, indexPath, opts.Username) {
hasPermittedPaths = true
response.Folders = append(response.Folders, subFolder)
}
response.Content = content
} else {
logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name)
}
for _, subFile := range subFiles {
indexPath := info.Path + subFile.Name
if opts.Access.Permitted(index.Path, indexPath, opts.Username) {
hasPermittedPaths = true
response.Files = append(response.Files, subFile)
}
}
if !hasPermittedPaths {
return response, errors.ErrPermissionDenied
}
}
response.FileInfo = *info
response.RealPath = realPath
response.Source = index.Name
if opts.Content {
processContent(response, index)
}
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
response.OnlyOfficeId = generateOfficeId(realPath)
}
if strings.HasPrefix(info.Type, "video") {
parentInfo, exists := index.GetReducedMetadata(filepath.Dir(info.Path), true)

return response, nil
}

func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index) {
isVideo := strings.HasPrefix(info.Type, "video")
if isVideo {
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(info.Path), true)
if exists {
response.DetectSubtitles(parentInfo)
info.DetectSubtitles(parentInfo)
err := info.LoadSubtitleContent()
if err != nil {
logger.Debug("failed to load subtitle content: " + err.Error())
}
}
return
}
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
content, err := getContent(info.RealPath)
if err != nil {
logger.Debugf("could not get content for file: "+info.RealPath, info.Name, err)
return
}
info.Content = content
} else {
logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name)
}
return response, nil
}

func generateOfficeId(realPath string) string {
Expand Down
12 changes: 7 additions & 5 deletions backend/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ func StartFilebrowser() {
if !keepGoing {
return
}
info, _ := utils.CheckForUpdates()
if info.LatestVersion != "" {
logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion)
logger.Infof("Release notes: %s", info.ReleaseNotes)
if !settings.Config.Server.DisableUpdateCheck {
info, _ := utils.CheckForUpdates()
if info.LatestVersion != "" {
logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion)
logger.Infof("Release notes: %s", info.ReleaseNotes)
}
go utils.StartCheckForUpdates()
}
go utils.StartCheckForUpdates()

// Create context and channels for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
Expand Down
14 changes: 7 additions & 7 deletions backend/common/settings/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (
)

type Auth struct {
TokenExpirationHours int `json:"tokenExpirationHours"` // the number of hours until the token expires. Default is 2 hours.
TokenExpirationHours int `json:"tokenExpirationHours"` // time in hours each web UI session token is valid for. Default is 2 hours.
Methods LoginMethods `json:"methods"`
Key string `json:"key"` // the key used to sign the JWT tokens. If not set, a random key will be generated.
AdminUsername string `json:"adminUsername"` // the username of the admin user. If not set, the default is "admin".
AdminPassword string `json:"adminPassword"` // the password of the admin user. If not set, the default is "admin".
TotpSecret string `json:"totpSecret"` // secret used to encrypt TOTP secrets
Key string `json:"key"` // secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
AdminUsername string `json:"adminUsername"` // secret: the username of the admin user. If not set, the default is "admin".
AdminPassword string `json:"adminPassword"` // secret: the password of the admin user. If not set, the default is "admin".
TotpSecret string `json:"totpSecret"` // secret: secret used to encrypt TOTP secrets
AuthMethods []string `json:"-"`
}

Expand Down Expand Up @@ -52,8 +52,8 @@ type Recaptcha struct {
// OpenID OAuth2.0
type OidcConfig struct {
Enabled bool `json:"enabled"` // whether to enable OIDC authentication
ClientID string `json:"clientId"` // client id of the OIDC application
ClientSecret string `json:"clientSecret"` // client secret of the OIDC application
ClientID string `json:"clientId"` // secret: client id of the OIDC application
ClientSecret string `json:"clientSecret"` // secret: client secret of the OIDC application
IssuerUrl string `json:"issuerUrl"` // authorization URL of the OIDC provider
Scopes string `json:"scopes"` // scopes to request from the OIDC provider
UserIdentifier string `json:"userIdentifier"` // the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
Expand Down
61 changes: 59 additions & 2 deletions backend/common/settings/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ func Initialize(configFile string) {
}

func setupFrontend(generate bool) {
if Config.Server.MinSearchLength == 0 {
Config.Server.MinSearchLength = 3
}
if !Config.Frontend.DisableDefaultLinks {
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
Text: fmt.Sprintf("(%v)", version.Version),
Expand All @@ -56,12 +59,15 @@ func setupFrontend(generate bool) {
})
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
Text: "Help",
Url: "https://github.com/gtsteffaniak/filebrowser/wiki",
Url: "help prompt",
})
}
if Config.Frontend.Description == "" {
Config.Frontend.Description = "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server"
}
Config.Frontend.Styling.LightBackground = FallbackColor(Config.Frontend.Styling.LightBackground, "#f5f5f5")
Config.Frontend.Styling.DarkBackground = FallbackColor(Config.Frontend.Styling.DarkBackground, "#141D24")
Config.Frontend.Styling.CustomCSS = readCustomCSS(Config.Frontend.Styling.CustomCSS)
Config.Frontend.Styling.CustomCSSRaw = readCustomCSS(Config.Frontend.Styling.CustomCSS)
Config.Frontend.Styling.CustomThemeOptions = map[string]CustomTheme{}
Config.Frontend.Styling.CustomThemes = map[string]CustomTheme{}
for name, theme := range Config.Frontend.Styling.CustomThemes {
Expand All @@ -85,6 +91,9 @@ func setupFrontend(generate bool) {
if !ok {
addCustomTheme("default", "The default theme", "")
}

// Load custom favicon if configured
loadCustomFavicon()
}

func getRealPath(path string) string {
Expand Down Expand Up @@ -511,6 +520,54 @@ func GetSources(u *users.User) []string {
return sources
}

func loadCustomFavicon() {
// Check if a custom favicon path is configured
if Config.Frontend.Favicon == "" {
logger.Debug("No custom favicon configured, using default")
return
}

// Get absolute path for the favicon
faviconPath, err := filepath.Abs(Config.Frontend.Favicon)
if err != nil {
logger.Warningf("Could not resolve favicon path '%v': %v", Config.Frontend.Favicon, err)
Config.Frontend.Favicon = "" // Unset invalid path
return
}

// Check if the favicon file exists and get info
stat, err := os.Stat(faviconPath)
if err != nil {
logger.Warningf("Could not access custom favicon file '%v': %v", faviconPath, err)
Config.Frontend.Favicon = "" // Unset invalid path
return
}

// Check file size (limit to 1MB for security)
const maxFaviconSize = 1024 * 1024 // 1MB
if stat.Size() > maxFaviconSize {
logger.Warningf("Favicon file '%v' is too large (%d bytes), maximum allowed is %d bytes", faviconPath, stat.Size(), maxFaviconSize)
Config.Frontend.Favicon = "" // Unset invalid path
return
}

// Validate file format based on extension
ext := strings.ToLower(filepath.Ext(faviconPath))
switch ext {
case ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp":
// Valid favicon formats
default:
logger.Warningf("Unsupported favicon format '%v', supported formats: .ico, .png, .jpg, .gif, .svg, .webp", ext)
Config.Frontend.Favicon = "" // Unset invalid path
return
}

// Update to absolute path and mark as valid
Config.Frontend.Favicon = faviconPath

logger.Infof("Successfully validated custom favicon at '%v' (%d bytes, %s)", faviconPath, stat.Size(), ext)
}

func modifyExcludeInclude(config *Source) {
normalize := func(s []string, checkExists bool) {
for i, v := range s {
Expand Down
Loading
Loading