Compare commits

...
67 changed files with 501 additions and 224 deletions

No files matched your search

+26
View File
@@ -2,6 +2,32 @@
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.7.10-beta
**OIDC change**: if you specify `oidc.userIdentifier: "username"`, originally this would map to `preferred_username` but now it maps to `username` explicitly. To maintain the same behavior update your config to `userIdentifier: "preferred_username"`. This was updated to allow for `username` to work as [some might need](https://github.com/gtsteffaniak/filebrowser/pull/789).
**New Features**:
- Added settings option to stop sidebar from automatically hiding on editor and previews. https://github.com/gtsteffaniak/filebrowser/issues/744
- Added more secrets loadable from environment variables. https://github.com/gtsteffaniak/filebrowser/issues/790
- Include/exclude files are checked for existence to assist with configuration, will show as warning if something is configured but doesn't exist.
- Added open in new tab link for preview items to view the raw picture, pdf, etc. Especially helpful for safari viewing PDF documents. https://github.com/gtsteffaniak/filebrowser/issues/734
- Added autoplay media toggle in user profile, to automatically play videos and audio.
**Notes**:
- Allowed to delete default admin user https://github.com/gtsteffaniak/filebrowser/issues/811 https://github.com/gtsteffaniak/filebrowser/issues/762
- Better try/catch error handling for user feedback for shares https://github.com/gtsteffaniak/filebrowser/issues/732
**BugFixes**:
- Fix share scope creation issue https://github.com/gtsteffaniak/filebrowser/issues/809
- Fix oidc token logout issue https://github.com/gtsteffaniak/filebrowser/issues/791
- Non-admin users OTP issue https://github.com/gtsteffaniak/filebrowser/issues/815
- Linewrap issue for a few cases https://github.com/gtsteffaniak/filebrowser/issues/810
- BaseUrl redirect issue with proxies https://github.com/gtsteffaniak/filebrowser/issues/796
- Fix exclude still shows up in ui issue https://github.com/gtsteffaniak/filebrowser/issues/797
- Copy/move functions are async https://github.com/gtsteffaniak/filebrowser/issues/812
- fix subtitle fetch issue https://github.com/gtsteffaniak/filebrowser/issues/766
- fix location memory issue for url encoded file names
## v0.7.9-beta
**New Features**:
+1 -1
View File
@@ -15,7 +15,7 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
- Ubuntu/Debian: `sudo apt-get install ffmpeg`
- macOS: `brew install ffmpeg`
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
- **mupdf-tools**: Required for PDF preview generation
- Ubuntu/Debian: `sudo apt-get install mupdf-tools`
- macOS: `brew install mupdf-tools`
+5
View File
@@ -5,6 +5,11 @@ server:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
config:
exclude:
folders:
- "excluded"
auth:
methods:
noauth: true
+2 -2
View File
@@ -76,7 +76,7 @@ func GenerateOtpForUser(user *users.User, userStore *users.Storage) (string, err
}
// set cache so verify can attempt to use it but not require it for user yet.
TotpCache.Set(user.Username, secretText+"||"+nonce)
url := "otpauth://totp/FileBrowser%20Quantum?secret=" + secretText
url := fmt.Sprintf("otpauth://totp/%v?secret=%v", "FileBrowser Quantum: "+user.Username, secretText)
return url, nil
}
@@ -191,7 +191,7 @@ func VerifyTotpCode(user *users.User, code string, userStore *users.Storage) err
user.TOTPNonce = totpNonce // The nonce if encrypted, or empty if plaintext
user.OtpEnabled = true // Enable OTP for the user
// save user
if err := userStore.Update(user, user.Permissions.Admin, "TOTPSecret", "TOTPNonce"); err != nil {
if err := userStore.Update(user, true, "TOTPSecret", "TOTPNonce"); err != nil {
logger.Error("error updating user with OTP token:", err)
return fmt.Errorf("error updating user with OTP token: %w", err)
}
+1 -1
View File
@@ -62,7 +62,7 @@ func runCLI() bool {
var asAdmin bool
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
setCmd.BoolVar(&asAdmin, "a", false, "Create a user as admin user, used in combination with -u")
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
-1
View File
@@ -17,7 +17,6 @@ var (
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrNoTotpProvided = errors.New("OTP code is required for user")
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not yet configured")
ErrUnauthorized = errors.New("user unauthorized")
+2 -2
View File
@@ -57,7 +57,7 @@ type OidcConfig struct {
ClientSecret string `json:"clientSecret"` // 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 user identifier to use for authentication. Default is "username", can be "email" or "username", or "phone"
UserIdentifier string `json:"userIdentifier"` // the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
DisableVerifyTLS bool `json:"disableVerifyTLS"` // disable TLS verification for the OIDC provider. This is insecure and should only be used for testing.
LogoutRedirectUrl string `json:"logoutRedirectUrl"` // if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
CreateUser bool `json:"createUser"` // create user if not exists
@@ -73,7 +73,7 @@ func validateOidcAuth() error {
return errors.New("OIDC is not enabled")
}
if oidcCfg.UserIdentifier == "" {
oidcCfg.UserIdentifier = "username"
oidcCfg.UserIdentifier = "preferred_username"
}
ctx := context.Background()
+60 -2
View File
@@ -97,6 +97,8 @@ func setupSources(generate bool) {
Config.Server.DefaultSource = source
}
}
modifyExcludeInclude(&source)
if source.Config.DefaultUserScope == "" {
source.Config.DefaultUserScope = "/"
}
@@ -226,7 +228,9 @@ func loadConfigWithDefaults(configFile string) error {
// Open and read the YAML file
yamlFile, err := os.Open(configFile)
if err != nil {
logger.Errorf("could not open config file '%v', using default settings.", configFile)
if configFile != "" {
logger.Errorf("could not open config file '%v', using default settings.", configFile)
}
Config.Server.Sources = []Source{
{
Path: ".",
@@ -267,7 +271,6 @@ func ValidateConfig(config Settings) error {
}
func loadEnvConfig() {
adminPassword, ok := os.LookupEnv("FILEBROWSER_ADMIN_PASSWORD")
if ok {
logger.Info("Using admin password from FILEBROWSER_ADMIN_PASSWORD environment variable")
@@ -284,6 +287,36 @@ func loadEnvConfig() {
Config.Integrations.Media.FfmpegPath = ffmpegPath
}
oidcClientId := os.Getenv("FILEBROWSER_OIDC_CLIENT_ID")
if oidcClientId != "" {
Config.Auth.Methods.OidcAuth.ClientID = oidcClientId
logger.Info("Using OIDC Client ID from FILEBROWSER_OIDC_CLIENT_ID environment variable")
}
oidcClientSecret := os.Getenv("FILEBROWSER_OIDC_CLIENT_SECRET")
if oidcClientSecret != "" {
Config.Auth.Methods.OidcAuth.ClientSecret = oidcClientSecret
logger.Info("Using OIDC Client Secret from FILEBROWSER_OIDC_CLIENT_SECRET environment variable")
}
jwtTokenSecret := os.Getenv("FILEBROWSER_JWT_TOKEN_SECRET")
if jwtTokenSecret != "" {
Config.Auth.Key = jwtTokenSecret
logger.Info("Using JWT Token Secret from FILEBROWSER_JWT_TOKEN_SECRET environment variable")
}
totpSecret := os.Getenv("FILEBROWSER_TOTP_SECRET")
if totpSecret != "" {
Config.Auth.TotpSecret = totpSecret
logger.Info("Using TOTP Secret from FILEBROWSER_TOTP_SECRET environment variable")
}
recaptchaSecret := os.Getenv("FILEBROWSER_RECAPTCHA_SECRET")
if recaptchaSecret != "" {
Config.Auth.Methods.PasswordAuth.Recaptcha.Secret = recaptchaSecret
logger.Info("Using ReCaptcha Secret from FILEBROWSER_RECAPTCHA_SECRET environment variable")
}
}
func setDefaults() Settings {
@@ -450,3 +483,28 @@ func GetSources(u *users.User) []string {
}
return sources
}
func modifyExcludeInclude(config *Source) {
normalize := func(s []string, checkExists bool) {
for i, v := range s {
s[i] = "/" + strings.Trim(v, "/")
// check if file/folder exists
if checkExists {
realPath, err := filepath.Abs(config.Path + s[i])
if err != nil {
logger.Warningf("could not get absolute path for %v: %v", s[i], err)
continue
}
if _, err := os.Stat(realPath); os.IsNotExist(err) {
logger.Warningf("configured exclude/include path %v does not exist", realPath)
}
}
}
}
normalize(config.Config.Exclude.Files, true)
normalize(config.Config.Exclude.Folders, true)
normalize(config.Config.Include.Files, true)
normalize(config.Config.Include.Folders, true)
}
+3 -3
View File
@@ -92,9 +92,9 @@ type SourceConfig struct {
}
type IndexFilter struct {
Files []string `json:"files"` // array of file names to include/exclude
Folders []string `json:"folders"` // array of folder names to include/exclude
FileEndsWith []string `json:"fileEndsWith"` // array of file names to include/exclude (eg "a.jpg")
Files []string `json:"files"` // list of file names to include/exclude Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (do not include source path, just the subpaths from the source path)
Folders []string `json:"folders"` // list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
FileEndsWith []string `json:"fileEndsWith"` // list of file names to include/exclude. Eg. "a.jpg"
}
type Frontend struct {
-10
View File
@@ -124,18 +124,8 @@ func (s *Storage) Save(user *User, changePass, disableScopeChange bool) error {
func (s *Storage) Delete(id interface{}) error {
switch id := id.(type) {
case string:
user, err := s.back.GetBy(id)
if err != nil {
return err
}
if user.ID == 1 {
return errors.ErrRootUserDeletion
}
return s.back.DeleteByUsername(id)
case uint:
if id == 1 {
return errors.ErrRootUserDeletion
}
return s.back.DeleteByID(id)
default:
return errors.ErrInvalidDataType
+3
View File
@@ -41,12 +41,14 @@ type Sorting struct {
}
type Preview struct {
DisableHideSidebar bool `json:"disableHideSidebar"` // disable the hide sidebar preview for previews and editors
HighQuality bool `json:"highQuality"` // generate high quality preview images
Image bool `json:"image"` // show real image as icon instead of generic photo icon
Video bool `json:"video"` // show preview image for video files
MotionVideoPreview bool `json:"motionVideoPreview"` // show multiple frames for videos in preview when hovering
Office bool `json:"office"` // show preview image for office files
PopUp bool `json:"popup"` // show larger popup preview when hovering
AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview
}
// User describes a user.
@@ -91,6 +93,7 @@ type NonAdminEditable struct {
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications
}
var PublicUser = User{
+2 -2
View File
@@ -6,7 +6,7 @@ require (
github.com/asdine/storm/v3 v3.2.1
github.com/coreos/go-oidc/v3 v3.14.1
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/gen2brain/go-fitz v1.24.14
github.com/gen2brain/go-fitz v1.24.15
github.com/go-playground/validator/v10 v10.26.0
github.com/goccy/go-yaml v1.18.0
github.com/golang-jwt/jwt/v4 v4.5.2
@@ -22,6 +22,7 @@ require (
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.39.0
golang.org/x/image v0.28.0
golang.org/x/mod v0.25.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.33.0
gopkg.in/yaml.v3 v3.0.1
@@ -238,7 +239,6 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
+2 -3
View File
@@ -204,8 +204,8 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
@@ -327,7 +327,6 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w=
github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8=
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
+6 -20
View File
@@ -50,7 +50,6 @@ func extractToken(r *http.Request) (string, error) {
// Check for Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
hasToken = true
// Split the header to get "Bearer {token}"
parts := strings.Split(authHeader, " ")
@@ -129,33 +128,20 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
// @Description logs a user out of the application.
// @Tags Auth
// @Success 302 {string} string "Redirect to redirect URL if configured in oidc config."
// @Router /api/auth/logout [get]
// @Router /api/auth/logout [post]
func logoutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = fmt.Sprintf("%s://%s", getScheme(r), r.Host)
}
logoutUrl := fmt.Sprintf("%s/login", origin)
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
oidcRedirectUrl := settings.Config.Auth.Methods.OidcAuth.LogoutRedirectUrl
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
logoutUrl := fmt.Sprintf("%vlogin", config.Server.BaseURL)
if d.user.LoginMethod == "proxy" {
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
if proxyRedirectUrl != "" {
logoutUrl = proxyRedirectUrl
}
} else {
} else if d.user.LoginMethod == "oidc" {
oidcRedirectUrl := config.Auth.Methods.OidcAuth.LogoutRedirectUrl
if oidcRedirectUrl != "" {
logoutUrl = oidcRedirectUrl
}
}
// clear cookie
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: "",
MaxAge: -1,
Path: "/",
})
http.Redirect(w, r, logoutUrl, http.StatusFound)
return 302, nil
}
+1 -1
View File
@@ -73,7 +73,7 @@ func StartHttp(ctx context.Context, storage *storage.Storage, shutdownComplete c
// Auth routes
api.HandleFunc("POST /auth/login", userWithoutOTP(loginHandler))
api.HandleFunc("GET /auth/logout", withoutUser(logoutHandler))
api.HandleFunc("POST /auth/logout", withUser(logoutHandler))
api.HandleFunc("POST /auth/signup", withoutUser(signupHandler))
api.HandleFunc("POST /auth/otp/generate", userWithoutOTP(generateOTPHandler))
api.HandleFunc("POST /auth/otp/verify", userWithoutOTP(verifyOTPHandler))
+1 -2
View File
@@ -53,7 +53,6 @@ func oidcLoginHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
nonce := utils.InsecureRandomIdentifier(16)
fbRedirect := r.URL.Query().Get("redirect")
state := fmt.Sprintf("%s:%s", nonce, fbRedirect)
authURL := oauth2Config.AuthCodeURL(state)
http.Redirect(w, r, authURL, http.StatusFound)
return 0, nil
@@ -195,7 +194,7 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
}
}
if loginUsername == "" {
logger.Error("No valid username found in ID token or UserInfo response.")
logger.Errorf("No valid username found for identifier '%v' in ID token or UserInfo response.", oidcCfg.UserIdentifier)
return http.StatusInternalServerError, fmt.Errorf("no valid username found in ID token or UserInfo response from claims")
}
isAdmin := false // Default to non-admin user
-3
View File
@@ -425,9 +425,6 @@ func inspectIndex(w http.ResponseWriter, r *http.Request) {
source := r.URL.Query().Get("source")
if source == "" {
source = config.Server.DefaultSource.Name
} else {
// decode url encoded source name
source, _ = url.QueryUnescape(source)
}
// Decode the URL-encoded path
path, _ := url.QueryUnescape(encodedPath)
+13 -4
View File
@@ -15,6 +15,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/database/share"
)
@@ -75,12 +76,16 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
sourcePath, ok := config.Server.NameToSource[source]
if !ok {
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", source)
}
s, err := store.Share.Gets(path, sourcePath.Path, d.user.ID)
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
return http.StatusForbidden, err
}
scopePath := utils.JoinPathAsUnix(userscope, path)
s, err := store.Share.Gets(scopePath, sourcePath.Path, d.user.ID)
if err == errors.ErrNotExist {
return renderJSON(w, r, []*share.Link{})
}
@@ -194,9 +199,13 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
sourceName = config.Server.DefaultSource.Name
}
source := config.Server.NameToSource[sourceName]
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source.Name)
if err != nil {
return http.StatusForbidden, err
}
scopePath := utils.JoinPathAsUnix(userscope, path)
s = &share.Link{
Path: path,
Path: scopePath,
Hash: secure_hash,
Source: source.Path, // path instead to persist accoss name change
Expire: expire,
-4
View File
@@ -118,10 +118,6 @@ func userDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext
return http.StatusForbidden, fmt.Errorf("cannot delete users without admin permissions")
}
if givenUserId == 1 {
return http.StatusForbidden, fmt.Errorf("cannot delete the default admin user")
}
// Delete the user
err := store.Users.Delete(givenUserId)
if err != nil {
-2
View File
@@ -22,8 +22,6 @@ func errToStatus(err error) int {
return http.StatusForbidden
case errors.Is(err, libErrors.ErrInvalidRequestParams):
return http.StatusBadRequest
case errors.Is(err, libErrors.ErrRootUserDeletion):
return http.StatusForbidden
default:
return http.StatusInternalServerError
}
+7 -7
View File
@@ -110,7 +110,6 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
// check if excluded from indexing
hidden := isHidden(dirInfo, idx.Path+adjustedPath)
if idx.shouldSkip(dirInfo.IsDir(), hidden, adjustedPath) {
logger.Debugf("skipping directory %s due to exclusion rules", adjustedPath)
return errors.ErrNotIndexed
}
@@ -153,7 +152,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
return nil
}
}
dirFileInfo, err2 := idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, quick, recursive)
dirFileInfo, err2 := idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, quick, recursive,true)
if err2 != nil {
return err2
}
@@ -187,7 +186,7 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
combinedPath = "/"
}
var response *iteminfo.FileInfo
response, err = idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, false, false)
response, err = idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, false, false,false)
if err != nil {
return nil, err
}
@@ -214,7 +213,7 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
}
func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjustedPath, combinedPath string, quick, recursive bool) (*iteminfo.FileInfo, error) {
func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjustedPath, combinedPath string, quick, recursive, checkSkip bool) (*iteminfo.FileInfo, error) {
// Read directory contents
files, err := dirInfo.Readdir(-1)
if err != nil {
@@ -229,7 +228,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
hidden := isHidden(file, idx.Path+combinedPath)
isDir := iteminfo.IsDirectory(file)
fullCombined := combinedPath + file.Name()
if idx.shouldSkip(isDir, hidden, fullCombined) && recursive {
if checkSkip && idx.shouldSkip(isDir, hidden, fullCombined) {
continue
}
itemInfo := &iteminfo.ItemInfo{
@@ -423,7 +422,6 @@ func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined string) boo
return true
}
}
// check exclusions
if isDir && slices.Contains(idx.Config.Exclude.Folders, fullCombined) {
return true
@@ -443,7 +441,9 @@ func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined string) boo
break
}
}
return shouldSkip
if shouldSkip {
return true
}
}
return false
-1
View File
@@ -41,7 +41,6 @@ var MuPdfConvertable = []string{
".svg", // SVG
".txt", // TXT
".docx", // DOCX
".ppt", // PPT
".pptx", // PPTX
".xlsx", // XLSX
".hwp", // HWP
+1 -1
View File
@@ -25,7 +25,7 @@ func (i *ExtendedFileInfo) DetectSubtitles(parentInfo *FileInfo) {
for _, subtitleExt := range SubtitleExts {
if strings.HasSuffix(f.Name, subtitleExt) {
i.Subtitles = append(i.Subtitles, parentInfo.Path+f.Name)
i.Subtitles = append(i.Subtitles, f.Name)
}
}
}
+13 -5
View File
@@ -16,7 +16,7 @@ const docTemplate = `{
"basePath": "{{.BasePath}}",
"paths": {
"/api/auth/logout": {
"get": {
"post": {
"description": "logs a user out of the application.",
"tags": [
"Auth"
@@ -1818,21 +1818,21 @@ const docTemplate = `{
"type": "object",
"properties": {
"fileEndsWith": {
"description": "array of file names to include/exclude (eg \"a.jpg\")",
"description": "list of file names to include/exclude. Eg. \"a.jpg\"",
"type": "array",
"items": {
"type": "string"
}
},
"files": {
"description": "array of file names to include/exclude",
"description": "list of file names to include/exclude Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (do not include source path, just the subpaths from the source path)",
"type": "array",
"items": {
"type": "string"
}
},
"folders": {
"description": "array of folder names to include/exclude",
"description": "list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
"type": "array",
"items": {
"type": "string"
@@ -1947,7 +1947,7 @@ const docTemplate = `{
"type": "string"
},
"userIdentifier": {
"description": "the user identifier to use for authentication. Default is \"username\", can be \"email\" or \"username\", or \"phone\"",
"description": "the field value to use as the username. Default is \"preferred_username\", can also be \"email\" or \"username\", or \"phone\"",
"type": "string"
}
}
@@ -2394,6 +2394,14 @@ const docTemplate = `{
"users.Preview": {
"type": "object",
"properties": {
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
},
"disableHideSidebar": {
"description": "disable the hide sidebar preview for previews and editors",
"type": "boolean"
},
"highQuality": {
"description": "generate high quality preview images",
"type": "boolean"
+13 -5
View File
@@ -5,7 +5,7 @@
},
"paths": {
"/api/auth/logout": {
"get": {
"post": {
"description": "logs a user out of the application.",
"tags": [
"Auth"
@@ -1807,21 +1807,21 @@
"type": "object",
"properties": {
"fileEndsWith": {
"description": "array of file names to include/exclude (eg \"a.jpg\")",
"description": "list of file names to include/exclude. Eg. \"a.jpg\"",
"type": "array",
"items": {
"type": "string"
}
},
"files": {
"description": "array of file names to include/exclude",
"description": "list of file names to include/exclude Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (do not include source path, just the subpaths from the source path)",
"type": "array",
"items": {
"type": "string"
}
},
"folders": {
"description": "array of folder names to include/exclude",
"description": "list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
"type": "array",
"items": {
"type": "string"
@@ -1936,7 +1936,7 @@
"type": "string"
},
"userIdentifier": {
"description": "the user identifier to use for authentication. Default is \"username\", can be \"email\" or \"username\", or \"phone\"",
"description": "the field value to use as the username. Default is \"preferred_username\", can also be \"email\" or \"username\", or \"phone\"",
"type": "string"
}
}
@@ -2383,6 +2383,14 @@
"users.Preview": {
"type": "object",
"properties": {
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
},
"disableHideSidebar": {
"description": "disable the hide sidebar preview for previews and editors",
"type": "boolean"
},
"highQuality": {
"description": "generate high quality preview images",
"type": "boolean"
+15 -6
View File
@@ -145,17 +145,20 @@ definitions:
settings.IndexFilter:
properties:
fileEndsWith:
description: array of file names to include/exclude (eg "a.jpg")
description: list of file names to include/exclude. Eg. "a.jpg"
items:
type: string
type: array
files:
description: array of file names to include/exclude
description: list of file names to include/exclude Eg. "folder1" or "file1.txt"
or "folder1/file1.txt" (do not include source path, just the subpaths from
the source path)
items:
type: string
type: array
folders:
description: array of folder names to include/exclude
description: list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder"
(do not include source path, just the subpaths from the source path)
items:
type: string
type: array
@@ -239,8 +242,8 @@ definitions:
description: scopes to request from the OIDC provider
type: string
userIdentifier:
description: the user identifier to use for authentication. Default is "username",
can be "email" or "username", or "phone"
description: the field value to use as the username. Default is "preferred_username",
can also be "email" or "username", or "phone"
type: string
type: object
settings.OnlyOffice:
@@ -571,6 +574,12 @@ definitions:
type: object
users.Preview:
properties:
autoplayMedia:
description: autoplay media files in preview
type: boolean
disableHideSidebar:
description: disable the hide sidebar preview for previews and editors
type: boolean
highQuality:
description: generate high quality preview images
type: boolean
@@ -695,7 +704,7 @@ info:
contact: {}
paths:
/api/auth/logout:
get:
post:
description: logs a user out of the application.
responses:
"302":
+3 -1
View File
@@ -66,7 +66,7 @@ auth:
clientSecret: "" # client secret of the OIDC application
issuerUrl: "" # authorization URL of the OIDC provider
scopes: "" # scopes to request from the OIDC provider
userIdentifier: "" # the user identifier to use for authentication. Default is "username", can be "email" or "username", or "phone"
userIdentifier: "" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification for the OIDC provider. This is insecure and should only be used for testing.
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
createUser: false # create user if not exists
@@ -103,12 +103,14 @@ userDefaults:
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
disableHideSidebar: false
highQuality: false
image: false
video: false
motionVideoPreview: false
office: false
popup: false
autoplayMedia: false
permissions:
api: false
admin: false
+1 -1
View File
@@ -149,4 +149,4 @@ async function syncAllTranslations() {
syncAllTranslations().catch(error => {
console.error("\n❌ An error occurred during translation synchronization:", error);
process.exit(1);
});
});
+3
View File
@@ -182,6 +182,9 @@ export async function moveCopy(
// Await all promises and ensure errors propagate
await Promise.all(promises)
notify.showSuccess(
action === 'copy' ? 'Resources copied successfully' : 'Resources moved successfully'
)
} catch (err) {
notify.showError(err.message || 'Error moving/copying resources')
throw err // Re-throw the error to propagate it back to the caller
+16 -1
View File
@@ -103,6 +103,7 @@
class="button no-select"
:class="{ 'dark-mode': isDarkMode }"
>
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('buttons.openFile')" @action="goToRaw()" />
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
@@ -118,7 +119,7 @@ import { onlyOfficeUrl } from "@/utils/constants.js";
import buttons from "@/utils/buttons";
import { notify } from "@/notify";
import { eventBus } from "@/store/eventBus";
import { filesApi } from "@/api";
export default {
name: "ContextMenu",
components: {
@@ -135,6 +136,10 @@ export default {
noItems() {
return !this.showEdit && !this.showSave && !this.showDelete;
},
showGoToRaw() {
return getters.currentView() == "preview" ||
getters.currentView() == "markdownViewer"
},
showEdit() {
return getters.currentView() == "markdownViewer" && state.user.permissions.modify;
},
@@ -257,6 +262,16 @@ export default {
startDownload() {
downloadFiles();
},
goToRaw() {
const downloadUrl = filesApi.getDownloadURL(
state.req.source,
state.req.path,
true,
false
);
window.open(downloadUrl, "_blank");
mutations.closeHovers();
},
async edit() {
window.location.hash = "#edit";
},
+2 -2
View File
@@ -129,7 +129,7 @@
<a :href="getRelative(s.path)" @contextmenu="addSelected(event, s)">
<Icon :mimetype="s.type" :filename="s.name" />
<span class="text-container">
{{ basePath(s.path, s.type == "directory") }}/{{ baseName(s.path) }}/<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
{{ basePath(s.path, s.type == "directory") }}{{ baseName(s.path) }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</span>
<div class="filesize">{{ humanSize(s.size) }}</div>
</a>
@@ -420,7 +420,7 @@ export default {
if (!isDir) {
result = url.removeLeadingSlash(result); // fix weird rtl thing
}
return result;
return result + "/";
},
baseName(str) {
let parts = url.removeTrailingSlash(str).split("/");
-1
View File
@@ -85,7 +85,6 @@ export default {
".svg": true,
".txt": true,
".docx": true,
".ppt": true,
".pptx": true,
".xlsx": true,
".hwp": true,
@@ -107,7 +107,7 @@ export default {
return !this.isSelected && this.reducedOpacity;
},
getID() {
return url.base64Encode(this.name);
return url.base64Encode(encodeURIComponent(this.name));
},
quickNav() {
return state.user.singleClick && !state.multiple;
+2 -5
View File
@@ -137,14 +137,11 @@ export default {
});
} else {
// Await the action call for non-conflicting cases
await action(overwrite, rename);
action(overwrite, rename);
}
mutations.closeHovers();
mutations.setSearch(false);
notify.showSuccess("Successfully copied file/folder, redirecting...");
setTimeout(() => {
this.$router.push(this.dest);
}, 1000);
notify.showSuccess("Server is copying files...");
} catch (error) {
notify.showError(error);
}
+2 -5
View File
@@ -136,14 +136,11 @@ export default {
});
} else {
// Await the action call for non-conflicting cases
await action(overwrite, rename);
action(overwrite, rename);
}
mutations.closeHovers();
mutations.setSearch(false);
notify.showSuccess("Successfully moved file/folder, redirecting...");
setTimeout(() => {
this.$router.push(this.dest);
}, 1000);
notify.showSuccess("Server is moving files...");
} catch (error) {
notify.showError(error);
}
+33 -25
View File
@@ -212,35 +212,43 @@ export default {
},
methods: {
async submit() {
let isPermanent = !this.time || this.time === 0;
let res = null;
if (isPermanent) {
res = await shareApi.create(this.subpath, this.source, this.password);
} else {
res = await shareApi.create(
this.subpath,
this.source,
this.password,
this.time.toString(),
this.unit
);
try {
let isPermanent = !this.time || this.time == 0;
let res = null;
if (isPermanent) {
res = await shareApi.create(this.subpath, this.source, this.password);
} else {
res = await shareApi.create(
this.subpath,
this.source,
this.password,
this.time.toString(),
this.unit
);
}
this.links.push(res);
this.sort();
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (err) {
notify.showError(err);
}
this.links.push(res);
this.sort();
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true;
},
async deleteLink(event, link) {
event.preventDefault();
await shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length === 0) {
this.listing = false;
try {
await shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length === 0) {
this.listing = false;
}
} catch (err) {
notify.showError(err);
}
},
humanTime(time) {
@@ -39,7 +39,6 @@
</div>
<div
style="display: flex; flex-direction: column"
v-if="stateUser.username == user.username"
>
<div class="settings-items">
<ToggleSwitch class="item" v-model="user.otpEnabled" :name="$t('otp.name')" />
+6
View File
@@ -1,4 +1,10 @@
@media (max-width: 800px) {
#listingView .header .name,
#listingView.list .item .name,
#listingView.compact .item .name {
width: 25%;
}
#listingView.list .item div:last-of-type{
display:block;
width:100%;
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "الإعدادات",
"signup": "Signup",
"siteSettings": "إعدادات الموقع",
"sources": "المصادر"
"sources": "المصادر",
"updateIsAvailable": "يتوفر تحديث!"
},
"success": {
"linkCopied": "تم نسخ الملف"
@@ -370,7 +371,10 @@
"previewOffice": "معاينة الملفات المكتبية",
"popupPreview": "تمكين المعاينة المنبثقة",
"filePreviewOptions": "خيارات معاينة الملف",
"editOffice": "تحرير/عرض ملفات المكتب"
"editOffice": "تحرير/عرض ملفات المكتب",
"disableUpdateNotifications": "تعطيل إظهار شعار تحديث الإشعارات في الشريط الجانبي.",
"disableHideSidebar": "تعطيل إخفاء الشريط الجانبي تلقائيًا عند معاينة الملفات أو تحريرها.",
"autoplayMedia": "التشغيل التلقائي لملفات الوسائط في المعاينة"
},
"otp": {
"name": "المصادقة الثنائية",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Einstellungen",
"signup": "Registrieren",
"siteSettings": "Seiteneinstellungen",
"sources": "Quellen"
"sources": "Quellen",
"updateIsAvailable": "Ein Update ist verfügbar!"
},
"success": {
"linkCopied": "Link wurde kopiert!"
@@ -370,7 +371,10 @@
"previewOffice": "Vorschau von Office-Dateien",
"popupPreview": "Pop-up-Vorschau einschalten",
"filePreviewOptions": "Optionen für die Dateivorschau",
"editOffice": "Office-Dateien bearbeiten/anzeigen"
"editOffice": "Office-Dateien bearbeiten/anzeigen",
"disableUpdateNotifications": "Deaktivieren Sie die Anzeige des Banners für Benachrichtigungsaktualisierungen in der Seitenleiste.",
"disableHideSidebar": "Deaktivieren Sie das automatische Ausblenden der Seitenleiste bei der Vorschau oder Bearbeitung von Dateien.",
"autoplayMedia": "Autoplay von Mediendateien in der Vorschau"
},
"otp": {
"name": "Zwei-Faktoren-Authentifizierung",
+6 -2
View File
@@ -305,7 +305,8 @@
"settings": "Ρυθμίσεις",
"signup": "Εγγραφή",
"siteSettings": "Ρυθμίσεις ιστότοπου",
"sources": "Πηγές"
"sources": "Πηγές",
"updateIsAvailable": "Μια ενημέρωση είναι διαθέσιμη!"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
@@ -371,7 +372,10 @@
"previewOffice": "Προεπισκόπηση αρχείων γραφείου",
"popupPreview": "Ενεργοποίηση αναδυόμενης προεπισκόπησης",
"filePreviewOptions": "Επιλογές προεπισκόπησης αρχείων",
"editOffice": "Επεξεργασία/προβολή αρχείων γραφείου"
"editOffice": "Επεξεργασία/προβολή αρχείων γραφείου",
"disableUpdateNotifications": "Απενεργοποιήστε την εμφάνιση του banner ενημέρωσης ειδοποιήσεων στην πλαϊνή γραμμή.",
"disableHideSidebar": "Απενεργοποίηση της αυτόματης απόκρυψης της πλευρικής γραμμής κατά την προεπισκόπηση ή την επεξεργασία αρχείων.",
"autoplayMedia": "Αυτόματη αναπαραγωγή αρχείων πολυμέσων στην προεπισκόπηση"
},
"otp": {
"name": "Αυθεντικοποίηση δύο παραγόντων",
+5 -2
View File
@@ -198,7 +198,9 @@
"editOffice": "Edit/view office files",
"popupPreview": "Enable pop-up preview",
"filePreviewOptions": "File preview options",
"disableUpdateNotifications": "Disable showing notification update banner in sidebar."
"disableUpdateNotifications": "Disable showing notification update banner in sidebar.",
"disableHideSidebar": "Disable automatically hiding sidebar when previewing or editing files.",
"autoplayMedia": "Autoplay media files in preview"
},
"settings": {
"enterPassword": "Enter password",
@@ -298,7 +300,8 @@
"userManagement": "User Management",
"userUpdated": "User updated!",
"username": "Username",
"users": "Users"
"users": "Users",
"adminOptions": "Admin user options"
},
"sidebar": {
"help": "Help",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Ajustes",
"signup": "Registrate",
"siteSettings": "Ajustes del sitio",
"sources": "Fuentes"
"sources": "Fuentes",
"updateIsAvailable": "Ya está disponible la actualización."
},
"success": {
"linkCopied": "¡Link copiado!"
@@ -370,7 +371,10 @@
"previewOffice": "Vista previa de archivos ofimáticos",
"popupPreview": "Activar la vista previa emergente",
"filePreviewOptions": "Opciones de previsualización de archivos",
"editOffice": "Editar/ver archivos ofimáticos"
"editOffice": "Editar/ver archivos ofimáticos",
"disableUpdateNotifications": "Desactivar la visualización del banner de actualización de notificaciones en la barra lateral.",
"disableHideSidebar": "Desactivar la ocultación automática de la barra lateral al previsualizar o editar archivos.",
"autoplayMedia": "Reproducción automática de archivos multimedia en vista previa"
},
"otp": {
"name": "Autenticación de dos factores",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Paramètres",
"signup": "S'inscrire",
"siteSettings": "Paramètres du site",
"sources": "Sources d'information"
"sources": "Sources d'information",
"updateIsAvailable": "Une mise à jour est disponible !"
},
"success": {
"linkCopied": "Lien copié !"
@@ -370,7 +371,10 @@
"previewOffice": "Prévisualisation des fichiers bureautique",
"popupPreview": "Activer la prévisualisation dans les fenêtres contextuelles",
"editOffice": "Editer/visualiser des fichiers bureautiques",
"filePreviewOptions": "Options de prévisualisation des fichiers"
"filePreviewOptions": "Options de prévisualisation des fichiers",
"disableUpdateNotifications": "Désactiver l'affichage de la bannière de mise à jour des notifications dans la barre latérale.",
"disableHideSidebar": "Désactiver le masquage automatique de la barre latérale lors de la prévisualisation ou de l'édition de fichiers.",
"autoplayMedia": "Lecture automatique des fichiers multimédias dans l'aperçu"
},
"otp": {
"name": "Authentification à deux facteurs",
+6 -2
View File
@@ -298,7 +298,8 @@
"settings": "הגדרות",
"signup": "הרשמה",
"siteSettings": "הגדרות",
"sources": "מקורות"
"sources": "מקורות",
"updateIsAvailable": "עדכון זמין!"
},
"success": {
"linkCopied": "הקישור הועתק!"
@@ -366,7 +367,10 @@
"highQualityPreview": "תצוגה מקדימה באיכות גבוהה",
"editOffice": "ערוך/הצג קבצי Office",
"popupPreview": "הפעל תצוגה מקדימה מוקפצת",
"filePreviewOptions": "אפשרויות תצוגה מקדימה של קבצים"
"filePreviewOptions": "אפשרויות תצוגה מקדימה של קבצים",
"disableUpdateNotifications": "השבת הצגת כרטיסיית עדכון הודעות בסרגל הצד.",
"disableHideSidebar": "השבת הסתרה אוטומטית של סרגל הצד בעת תצוגה מקדימה או עריכת קבצים.",
"autoplayMedia": "הפעל קבצי מדיה באופן אוטומטי בתצוגה מקדימה"
},
"otp": {
"name": "אימות דו-גורמי",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Beállítások",
"signup": "Regisztráció",
"siteSettings": "Oldalbeállítások",
"sources": "Források"
"sources": "Források",
"updateIsAvailable": "Elérhető a frissítés!"
},
"success": {
"linkCopied": "Link másolva!"
@@ -370,7 +371,10 @@
"previewOffice": "Office fájlok előnézete",
"popupPreview": "Pop-up előnézet engedélyezése",
"filePreviewOptions": "Fájl előnézeti beállítások",
"editOffice": "Office fájlok szerkesztése/megjelenítése"
"editOffice": "Office fájlok szerkesztése/megjelenítése",
"disableUpdateNotifications": "Értesítési frissítés banner megjelenítésének kikapcsolása az oldalsávban.",
"disableHideSidebar": "Az oldalsáv automatikus elrejtésének kikapcsolása fájlok előnézeténél vagy szerkesztésénél.",
"autoplayMedia": "Médiafájlok automatikus lejátszása előnézetben"
},
"otp": {
"name": "Kétfaktoros hitelesítés",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Impostazioni",
"signup": "Registrati",
"siteSettings": "Impostazioni del sito",
"sources": "Fonti"
"sources": "Fonti",
"updateIsAvailable": "È disponibile un aggiornamento!"
},
"success": {
"linkCopied": "Link copiato!"
@@ -370,7 +371,10 @@
"previewOffice": "Anteprima dei file office",
"popupPreview": "Abilita l'anteprima a comparsa",
"filePreviewOptions": "Opzioni di anteprima dei file",
"editOffice": "Modificare/visualizzare i file dell'ufficio"
"editOffice": "Modificare/visualizzare i file dell'ufficio",
"disableUpdateNotifications": "Disattivare la visualizzazione del banner di aggiornamento delle notifiche nella barra laterale.",
"disableHideSidebar": "Disabilita la possibilità di nascondere automaticamente la barra laterale durante l'anteprima o la modifica dei file.",
"autoplayMedia": "Riproduzione automatica dei file multimediali nell'anteprima"
},
"otp": {
"name": "Autenticazione a due fattori",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "設定",
"signup": "Signup",
"siteSettings": "サイト設定",
"sources": "情報源"
"sources": "情報源",
"updateIsAvailable": "アップデートがある!"
},
"success": {
"linkCopied": "リンクがコピーされました!"
@@ -370,7 +371,10 @@
"previewOffice": "オフィスファイルのプレビュー",
"popupPreview": "ポップアップ・プレビューを有効にする",
"filePreviewOptions": "ファイルプレビューオプション",
"editOffice": "オフィスファイルの編集/閲覧"
"editOffice": "オフィスファイルの編集/閲覧",
"disableUpdateNotifications": "サイドバーに通知更新バナーを表示しないようにしました。",
"disableHideSidebar": "ファイルのプレビューや編集時にサイドバーを自動的に非表示にします。",
"autoplayMedia": "プレビューでメディアファイルを自動再生"
},
"otp": {
"name": "二要素認証",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "설정",
"signup": "가입하기",
"siteSettings": "사이트 설정",
"sources": "출처"
"sources": "출처",
"updateIsAvailable": "업데이트를 사용할 수 있습니다!"
},
"success": {
"linkCopied": "링크가 복사되었습니다!"
@@ -370,7 +371,10 @@
"previewOffice": "오피스 파일 미리 보기",
"popupPreview": "팝업 미리 보기 사용",
"filePreviewOptions": "파일 미리 보기 옵션",
"editOffice": "오피스 파일 편집/보기"
"editOffice": "오피스 파일 편집/보기",
"disableUpdateNotifications": "사이드바에 알림 업데이트 배너 표시를 비활성화합니다.",
"disableHideSidebar": "파일을 미리 보거나 편집할 때 사이드바 자동 숨기기를 비활성화합니다.",
"autoplayMedia": "미리보기에서 미디어 파일 자동 재생"
},
"otp": {
"name": "2단계 인증",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Ustawienia",
"signup": "Rejestracja",
"siteSettings": "Ustawienia Strony",
"sources": "Źródła"
"sources": "Źródła",
"updateIsAvailable": "Aktualizacja jest już dostępna!"
},
"success": {
"linkCopied": "Link Skopiowany!"
@@ -370,7 +371,10 @@
"previewOffice": "Podgląd plików biurowych",
"popupPreview": "Włącz wyskakujący podgląd",
"filePreviewOptions": "Opcje podglądu plików",
"editOffice": "Edycja/przeglądanie plików biurowych"
"editOffice": "Edycja/przeglądanie plików biurowych",
"disableUpdateNotifications": "Wyłącz wyświetlanie banera aktualizacji powiadomień na pasku bocznym.",
"disableHideSidebar": "Wyłącz automatyczne ukrywanie paska bocznego podczas podglądu lub edycji plików.",
"autoplayMedia": "Automatyczne odtwarzanie plików multimedialnych w podglądzie"
},
"otp": {
"name": "Uwierzytelnianie dwuskładnikowe",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Configurações",
"signup": "Cadastrar",
"siteSettings": "Configurações do site",
"sources": "Fontes"
"sources": "Fontes",
"updateIsAvailable": "Uma atualização está disponível!"
},
"success": {
"linkCopied": "Link copiado!"
@@ -370,7 +371,10 @@
"previewOffice": "Visualizar arquivos do Office",
"popupPreview": "Ativar visualização pop-up",
"filePreviewOptions": "Opções de visualização de arquivos",
"editOffice": "Editar/visualizar arquivos de escritório"
"editOffice": "Editar/visualizar arquivos de escritório",
"disableUpdateNotifications": "Desativar a exibição do banner de atualização de notificação na barra lateral.",
"disableHideSidebar": "Desative a ocultação automática da barra lateral ao visualizar ou editar arquivos.",
"autoplayMedia": "Reprodução automática de arquivos de mídia na visualização"
},
"otp": {
"name": "Autenticação de dois fatores",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Setări",
"signup": "Înregistrare",
"siteSettings": "Setări site",
"sources": "Surse"
"sources": "Surse",
"updateIsAvailable": "O actualizare este disponibilă!"
},
"success": {
"linkCopied": "Legătură copiată!"
@@ -370,7 +371,10 @@
"previewOffice": "Previzualizare fișiere office",
"popupPreview": "Activați previzualizarea pop-up",
"filePreviewOptions": "Opțiuni de previzualizare a fișierelor",
"editOffice": "Editarea/vizualizarea fișierelor office"
"editOffice": "Editarea/vizualizarea fișierelor office",
"disableUpdateNotifications": "Dezactivați afișarea bannerului de actualizare a notificărilor în bara laterală.",
"disableHideSidebar": "Dezactivați ascunderea automată a barei laterale la previzualizarea sau editarea fișierelor.",
"autoplayMedia": "Redarea automată a fișierelor media în previzualizare"
},
"otp": {
"name": "Autentificare cu doi factori",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Настройки",
"signup": "Зарегистрироваться",
"siteSettings": "Настройки сайта",
"sources": "Источники"
"sources": "Источники",
"updateIsAvailable": "Обновление доступно!"
},
"success": {
"linkCopied": "Ссылка скопирована!"
@@ -370,7 +371,10 @@
"previewOffice": "Предварительный просмотр офисных файлов",
"popupPreview": "Включить предварительный просмотр всплывающих окон",
"filePreviewOptions": "Параметры предварительного просмотра файлов",
"editOffice": "Редактирование/просмотр офисных файлов"
"editOffice": "Редактирование/просмотр офисных файлов",
"disableUpdateNotifications": "Отключите показ баннера обновлений в боковой панели.",
"disableHideSidebar": "Отключите автоматическое скрытие боковой панели при предварительном просмотре или редактировании файлов.",
"autoplayMedia": "Автовоспроизведение медиафайлов в режиме предварительного просмотра"
},
"otp": {
"name": "Двухфакторная аутентификация",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Nastavenia",
"signup": "Registrovať",
"siteSettings": "Nastavenia stránky",
"sources": "Zdroje"
"sources": "Zdroje",
"updateIsAvailable": "K dispozícii je aktualizácia!"
},
"success": {
"linkCopied": "Odkaz skopírovaný!"
@@ -370,7 +371,10 @@
"previewOffice": "Náhľad kancelárskych súborov",
"popupPreview": "Povolenie pop-up náhľadu",
"filePreviewOptions": "Možnosti náhľadu súborov",
"editOffice": "Úprava/zobrazenie kancelárskych súborov"
"editOffice": "Úprava/zobrazenie kancelárskych súborov",
"disableUpdateNotifications": "Zakázanie zobrazovania bannera s oznámením o aktualizácii v bočnom paneli.",
"disableHideSidebar": "Zakázanie automatického skrývania bočného panela pri náhľade alebo úprave súborov.",
"autoplayMedia": "Automatické prehrávanie mediálnych súborov v náhľade"
},
"otp": {
"name": "Dvojfaktorové overovanie",
+6 -2
View File
@@ -301,7 +301,8 @@
"settings": "Ayarlar",
"signup": "Kayıt",
"siteSettings": "Site ayarları!",
"sources": "Kaynaklar"
"sources": "Kaynaklar",
"updateIsAvailable": "Bir güncelleme mevcut!"
},
"success": {
"linkCopied": "Link kopyalandı!"
@@ -370,7 +371,10 @@
"previewOffice": "Ofis dosyalarını önizleme",
"popupPreview": "Açılır pencere önizlemesini etkinleştir",
"filePreviewOptions": "Dosya önizleme seçenekleri",
"editOffice": "Ofis dosyalarını düzenleme/görüntüleme"
"editOffice": "Ofis dosyalarını düzenleme/görüntüleme",
"disableUpdateNotifications": "Kenar çubuğunda bildirim güncelleme banner'ını göstermeyi devre dışı bırakın.",
"disableHideSidebar": "Dosyaları önizlerken veya düzenlerken kenar çubuğunu otomatik olarak gizlemeyi devre dışı bırakın.",
"autoplayMedia": "Medya dosyalarını önizlemede otomatik oynatma"
},
"otp": {
"name": "İki Faktörlü Kimlik Doğrulama",
+3 -1
View File
@@ -9,6 +9,7 @@ import { baseURL, name, oidcAvailable, passwordAvailable } from "@/utils/constan
import { getters, state } from "@/store";
import { mutations } from "@/store";
import { validateLogin } from "@/utils/auth";
import { removeLeadingSlash } from "@/utils/url";
import i18n from "@/i18n";
const titles = {
@@ -150,7 +151,8 @@ router.beforeResolve(async (to, from, next) => {
}
if (oidcAvailable) {
window.location.href = `/api/auth/oidc/login?redirect=${encodeURIComponent(to.fullPath)}`;
const modifiedPath = encodeURIComponent(baseURL+removeLeadingSlash(to.fullPath))
window.location.href = baseURL+`api/auth/oidc/login?redirect=${modifiedPath}`;
return;
}
}
+9 -4
View File
@@ -137,6 +137,14 @@ export const getters = {
},
isSidebarVisible: () => {
const currentView = getters.currentView()
const previewViews = [
'preview',
'markdownViewer',
'epubViewer',
'docViewer',
'onlyOfficeEditor',
'editor'
]
let visible =
(state.showSidebar || getters.isStickySidebar()) &&
state.user.username != 'publicUser'
@@ -152,10 +160,7 @@ export const getters = {
) {
visible = false
}
if (currentView == 'preview' || currentView == 'markdownViewer') {
visible = false
}
if (currentView == 'editor' || currentView == 'onlyOfficeEditor') {
if (previewViews.includes(currentView) && !state.user.preview?.disableHideSidebar) {
visible = false
}
return visible
-1
View File
@@ -217,7 +217,6 @@ export const mutations = {
if (value.username != "publicUser") {
mutations.setSources(value);
}
console.log("Setting current user:", value);
// Ensure locale exists and is valid
if (!value.locale) {
value.locale = i18n.detectLocale(); // Default to detected locale if missing
+20 -5
View File
@@ -56,11 +56,26 @@ export function generateRandomCode(length) {
return code;
}
export function logout() {
if (state.user.loginMethod == "oidc" || state.user.loginMethod == "proxy") {
let apiPath = getApiPath("api/auth/logout")
window.location.href = apiPath;
return
export async function logout() {
if (state.user.loginMethod === "oidc" || state.user.loginMethod === "proxy") {
try {
const res = await fetch(getApiPath("api/auth/logout"), {
method: "POST",
headers: {
Authorization: `Bearer ${state.jwt}`,
},
});
if (res.redirected) {
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
window.location.href = res.url;
return;
}
} catch (e) {
console.error(e);
}
}
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
-1
View File
@@ -112,7 +112,6 @@ export default {
redirect = baseURL + redirect;
}
this.loginURL += `?redirect=${encodeURIComponent(redirect)}`;
if (!recaptcha) return;
window.grecaptcha.ready(function () {
window.grecaptcha.render("recaptcha", {
+65 -13
View File
@@ -29,7 +29,20 @@
/>
</video>
<object v-else-if="previewType == 'pdf'" class="pdf" :data="raw"></object>
<div v-else-if="previewType == 'pdf'" class="pdf-wrapper">
<iframe class="pdf" :src="raw"></iframe>
<a
v-if="isMobileSafari"
:href="raw"
target="_blank"
class="button button--flat floating-btn"
>
<div>
<i class="material-icons">open_in_new</i>{{ $t("buttons.openFile") }}
</div>
</a>
</div>
<div v-else class="info">
<div class="title">
<i class="material-icons">feedback</i>
@@ -104,7 +117,6 @@ export default {
showNav: true,
navTimeout: null,
hoverNav: false,
autoPlay: false,
previousRaw: "",
nextRaw: "",
currentPrompt: null, // Replaces Vuex getter `currentPrompt`
@@ -112,6 +124,15 @@ export default {
};
},
computed: {
autoPlay() {
return state.user.preview.autoplayMedia;
},
isMobileSafari() {
const userAgent = window.navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
return isIOS && isSafari;
},
pdfConvertable() {
const ext = "." + state.req.name.split(".").pop().toLowerCase(); // Ensure lowercase and dot
const pdfConvertCompatibleFileExtensions = {
@@ -121,7 +142,6 @@ export default {
".cbz": true,
".svg": true,
".docx": true,
".ppt": true,
".pptx": true,
".xlsx": true,
".hwp": true,
@@ -201,14 +221,10 @@ export default {
return [];
}
let subs = [];
for (let subtitleFile of state.req.subtitles) {
if (state.serverHasMultipleSources) {
subtitleFile = "/files/" + state.req.source + subtitleFile;
} else {
subtitleFile = "/files" + subtitleFile;
}
for (const subtitleFile of state.req.subtitles) {
const ext = getFileExtension(subtitleFile);
const resp = await filesApi.fetchFiles(subtitleFile, true); // Fetch .srt file
const subUrl = url.removeLastDir(state.req.url) + "/" + subtitleFile;
const resp = await filesApi.fetchFiles(subUrl, true); // Fetch .srt file
let vttContent = resp.content;
// Convert SRT to VTT (assuming srt2vtt() does this)
vttContent = convertToVTT(ext, resp.content);
@@ -245,7 +261,7 @@ export default {
this.hoverNav = false;
this.$router.replace({ path: this.nextLink });
},
key(event) {
async key(event) {
if (getters.currentPromptName() != null) {
return;
}
@@ -269,9 +285,23 @@ export default {
}
},
async updatePreview() {
if (this.$refs.player && this.$refs.player.paused && !this.$refs.player.ended) {
this.autoPlay = false;
// Try to autoplay media, handle browser restrictions
if (this.autoPlay && (this.previewType === "video" || this.previewType === "audio")) {
this.$nextTick(() => {
if (this.$refs.player) {
const playPromise = this.$refs.player.play();
if (playPromise !== undefined) {
playPromise.catch((error) => {
if (this.$refs.player) {
this.$refs.player.muted = true;
this.$refs.player.play();
}
});
}
}
});
}
if (!this.listing) {
const path = url.removeLastDir(getters.routePath());
const res = await filesApi.fetchFiles(path);
@@ -348,3 +378,25 @@ export default {
},
};
</script>
<style scoped>
.pdf-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.pdf-wrapper .pdf {
width: 100%;
height: 100%;
border: 0;
}
.pdf-wrapper .floating-btn {
background: rgba(0, 0, 0, 0.5);
color: white;
}
.pdf-wrapper .floating-btn:hover {
background: rgba(0, 0, 0, 0.7);
}
</style>
+30 -10
View File
@@ -23,10 +23,7 @@
v-model="localuser.quickDownload"
:name="$t('profileSettings.showQuickDownload')"
/>
</div>
<h3>{{ $t("profileSettings.filePreviewOptions") }}</h3>
<div class="settings-items">
<ToggleSwitch
<ToggleSwitch
class="item"
v-model="localuser.preview.image"
:name="$t('profileSettings.previewImages')"
@@ -60,6 +57,19 @@
:name="$t('profileSettings.popupPreview')"
/>
</div>
<h3>{{ $t("profileSettings.filePreviewOptions") }}</h3>
<div class="settings-items">
<ToggleSwitch
class="item"
v-model="localuser.preview.autoplayMedia"
:name="$t('profileSettings.autoplayMedia')"
/>
<ToggleSwitch
class="item"
v-model="localuser.preview.disableHideSidebar"
:name="$t('profileSettings.disableHideSidebar')"
/>
</div>
<div v-if="hasOnlyOfficeEnabled">
<h3>{{ $t("settings.disableOfficePreview") }}</h3>
<p>
@@ -107,12 +117,7 @@
</button>
</div>
</div>
<ToggleSwitch
v-if="localuser.permissions?.admin"
class="item"
v-model="localuser.disableUpdateNotifications"
:name="$t('profileSettings.disableUpdateNotifications')"
/>
<h3>{{ $t("settings.themeColor") }}</h3>
<ButtonGroup
:buttons="colorChoices"
@@ -125,6 +130,15 @@
:locale="localuser.locale"
@update:locale="updateLocale"
></Languages>
<h3 v-if="user.permissions.admin">{{ $t("settings.adminOptions") }}</h3>
<div v-if="user.permissions.admin" class="settings-items">
<ToggleSwitch
v-if="localuser.permissions?.admin"
class="item"
v-model="localuser.disableUpdateNotifications"
:name="$t('profileSettings.disableUpdateNotifications')"
/>
</div>
</div>
</form>
</div>
@@ -175,6 +189,9 @@ export default {
},
},
computed: {
user() {
return state.user;
},
muPdfAvailable() {
return muPdfAvailable;
},
@@ -267,4 +284,7 @@ export default {
.card-content h3 {
text-align: center;
}
#officePreviewExt {
width: 80%;
}
</style>
@@ -76,10 +76,8 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('li[aria-label="myfolder"]').click();
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(1);
await page.locator('button[aria-label="Copy"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/myfolder/');
await expect(page.locator('#popup-notification-content')).toHaveText("Resources copied successfully");
await page.goto("/files/playwright-files/myfolder/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
// verify exists and copy again
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@@ -93,9 +91,9 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('input[aria-label="New Folder Name"]').fill('newfolder');
await page.locator('button[aria-label="Create"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
await expect(page).toHaveTitle(/.* - newfolder/);
await page.goBack();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
await expect(page).toHaveTitle(/.* - myfolder/);
await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" });
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
@@ -104,11 +102,9 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
await page.locator('li[aria-label="newfolder"]').click();
await page.locator('button[aria-label="Copy"]').click();
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
//await page.waitForURL('**/newfolder/');
//await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
await expect(page.locator('#popup-notification-content')).toHaveText("Resources copied successfully");
await page.goto("/files/playwright-files/myfolder/newfolder/");
await expect(page).toHaveTitle(/.* - newfolder/);
checkForErrors();
})
@@ -127,7 +123,6 @@ test("delete file", async({ page, checkForErrors, context }) => {
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Deleted item successfully! reloading...");
checkForErrors();
})
test("delete nested file prompt", async({ page, checkForErrors, context }) => {
@@ -52,10 +52,8 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('li[aria-label="myfolder"]').click();
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(1);
await page.locator('button[aria-label="Copy"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/myfolder/');
await expect(page.locator('#popup-notification-content')).toHaveText("Resources copied successfully");
await page.goto("/files/myfolder/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
// verify exists and copy again
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@@ -69,9 +67,9 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await page.locator('input[aria-label="New Folder Name"]').fill('newfolder');
await page.locator('button[aria-label="Create"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
await expect(page).toHaveTitle(/.* - newfolder/);
await page.goBack();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
await expect(page).toHaveTitle(/.* - myfolder/);
await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" });
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
@@ -80,11 +78,9 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
await page.locator('li[aria-label="newfolder"]').click();
await page.locator('button[aria-label="Copy"]').click();
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/newfolder/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
await expect(page.locator('#popup-notification-content')).toHaveText("Resources copied successfully");
await page.goto("/files/myfolder/newfolder/");
await expect(page).toHaveTitle(/.* - newfolder/);
checkForErrors();
})
@@ -0,0 +1,12 @@
import { test, expect } from "../test-setup";
test("navigate folders", async ({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
// excluded folder should not be visible in the file list
await expect(page.locator('a[aria-label="excluded"]')).toHaveCount(0);
await page.goto("/files/excluded");
const msg = "Error: {\"status\":500,\"message\":\"could not refresh file info: directory or item excluded from indexing\"}"
await expect(page.locator('#popup-notification-content')).toHaveText(msg);
checkForErrors(2,1);
});
Whitespace-only changes.
+1 -1
View File
@@ -28,7 +28,7 @@ run: build-frontend
else \
sed -i '/func init/,+3d' ./swagger/docs/docs.go; \
fi && \
FILEBROWSER_NO_EMBEDED=true CGO_ENABLED=1 go run -tags mupdf \
FILEBROWSER_NO_EMBEDED=true CGO_ENABLED=1 go run \
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml
build-frontend: