Compare commits

...
20 Commits
Author SHA1 Message Date
Graham Steffaniak 7619840a81 annotations (#874) 2025-06-29 13:17:53 -05:00
Graham Steffaniak efbe619f0a fix typo in json annotation (#873) 2025-06-29 10:57:13 -05:00
Graham Steffaniak 57d3749e40 File upload (#869) 2025-06-28 08:49:39 -05:00
Graham Steffaniak be27d9cff0 updated settings (#866) 2025-06-27 21:44:42 -05:00
Graham Steffaniak ea113ee3b6 Upload files bug (#862) 2025-06-27 18:01:01 -05:00
Graham Steffaniak da9414d7f2 updated with changes (#861) 2025-06-27 17:44:10 -05:00
Graham Steffaniak 3d1525c54b updated oidc login (#860) 2025-06-27 14:31:51 -05:00
Graham Steffaniak 4fb44765b9 bugfixes for 0.7.11 (#859) 2025-06-27 13:45:39 -05:00
Graham Steffaniak 40963b26db Merge branch 'dev/v0.7.11' of github.com:gtsteffaniak/filebrowser into dev/v0.7.11 2025-06-27 11:30:49 -05:00
Graham Steffaniak 6663a3097d updated 2025-06-27 11:30:11 -05:00
Graham Steffaniak 0d36810a3e Beta/v0.7.10 retagged changes (#833) 2025-06-27 11:30:11 -05:00
Graham Steffaniak 66f0338e8c updated swagger docs (#857) 2025-06-27 11:29:29 -05:00
Graham Steffaniak 6dd770492e updated 2025-06-27 11:08:06 -05:00
Graham Steffaniak e033af1305 updated 2025-06-21 15:27:59 -05:00
Graham Steffaniak ddd4ce2b75 updated settings (#822) 2025-06-21 15:26:54 -05:00
Graham Steffaniak 9c618c2d4c updated with autoplay (#821) 2025-06-21 13:24:04 -05:00
Graham Steffaniak 4a461e5199 More bugfixes (#820) 2025-06-21 12:13:13 -05:00
Graham Steffaniak 2db27856e0 More bugfixes (#819) 2025-06-20 23:02:01 -05:00
Graham Steffaniak ec2654a774 updated with oidc logout fix (#818) 2025-06-20 15:59:05 -05:00
Graham Steffaniak 4e441168d7 updated with share scope fix (#817) 2025-06-20 14:35:34 -05:00
42 changed files with 1532 additions and 1213 deletions

No files matched your search

+25
View File
@@ -2,6 +2,31 @@
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.11-beta
**Breaking Changes**:
- `auth.resetAdminOnStart` has been removed. Instead, if you have `auth.adminPassword` set it will always be reset on startup. If you want to change your default admin password afterwards, make sure to unset `auth.adminPassword` so it doesn't get reset on startup.
- renamed include/exclude rules see [updated example wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration-And-Examples#example-advanced-source-config)!
**New Features**:
- more comprehensive exclude/include rules (see example wiki above).
- include/exclude parts of folder names as well https://github.com/gtsteffaniak/filebrowser/issues/854
- include/exclude file or folder names globally.
- `source.config.neverWatchPaths` is now functional -- a list of paths that get indexed initially, but skips re-indexing. Useful for directories you don't expect to change ever, still show up in search but get don't contribute to indexing time after initial indexing.
**Notes**:
- updated swagger docs https://github.com/gtsteffaniak/filebrowser/issues/849
**BugFixes**:
- fix version update notification for binary https://github.com/gtsteffaniak/filebrowser/issues/836
- ctrl-click cache issue https://github.com/gtsteffaniak/filebrowser/issues/735
- fix admin user reset OIDC user https://github.com/gtsteffaniak/filebrowser/issues/811 https://github.com/gtsteffaniak/filebrowser/issues/851
- fix windows and binary muPdf issue https://github.com/gtsteffaniak/filebrowser/issues/744
- fix logout oidc issue https://github.com/gtsteffaniak/filebrowser/issues/829 https://github.com/gtsteffaniak/filebrowser/issues/662
- file name upload bug https://github.com/gtsteffaniak/filebrowser/issues/662
- could not create share with absolute timestamps enabled https://github.com/gtsteffaniak/filebrowser/issues/764
- context menu off screen issue https://github.com/gtsteffaniak/filebrowser/issues/828
## 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).
+1 -1
View File
@@ -7,7 +7,7 @@ server:
- path: "../frontend/tests/playwright-files"
config:
exclude:
folders:
folderPaths:
- "excluded"
auth:
+1 -1
View File
@@ -12,7 +12,7 @@ builds:
- mupdf
- musl
ldflags: &ldflags
- -s -w -X github.com/gtsteffaniak/filebrowser/backend/common/version.Version={{ .Version }} -X github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA={{ .ShortCommit }}
- -s -w -X github.com/gtsteffaniak/filebrowser/backend/common/version.Version=v{{ .Version }} -X github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
-278
View File
@@ -1,278 +0,0 @@
package auth
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
"github.com/gtsteffaniak/go-logger/logger"
)
type hookCred struct {
Password string `json:"password"`
Username string `json:"username"`
}
// HookAuth is a hook implementation of an Auther.
type HookAuth struct {
Users users.Store `json:"-"`
Settings *settings.Settings `json:"-"`
Server *settings.Server `json:"-"`
Cred hookCred `json:"-"`
Fields hookFields `json:"-"`
Command string `json:"command"`
}
// Auth authenticates the user via a json in content body.
func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
var cred hookCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
logger.Error("decode body error")
return nil, os.ErrPermission
}
a.Users = usr
a.Settings = &settings.Config
a.Server = &settings.Config.Server
a.Cred = cred
action, err := a.RunCommand()
if err != nil {
return nil, err
}
logger.Debugf("hook auth %v", action)
switch action {
case "auth":
u, err := a.SaveUser()
if err != nil {
return nil, err
}
return u, nil
case "block":
logger.Error("block error")
return nil, os.ErrPermission
case "pass":
logger.Error("pass error")
u, err := a.Users.Get(a.Cred.Username)
if err != nil {
return nil, fmt.Errorf("unable to get user from store: %v", err)
}
err = users.CheckPwd(cred.Password, u.Password)
if err != nil {
return nil, err
}
return u, nil
default:
return nil, fmt.Errorf("invalid hook action: %s", action)
}
}
// LoginPage tells that hook auth requires a login page.
func (a *HookAuth) LoginPage() bool {
return true
}
// RunCommand starts the hook command and returns the action
func (a *HookAuth) RunCommand() (string, error) {
command := strings.Split(a.Command, " ")
envMapping := func(key string) string {
switch key {
case "USERNAME":
return a.Cred.Username
case "PASSWORD":
return a.Cred.Password
default:
return os.Getenv(key)
}
}
for i, arg := range command {
if i == 0 {
continue
}
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()
if err != nil {
return "", err
}
a.GetValues(string(out))
return a.Fields.Values["hook.action"], nil
}
// GetValues creates a map with values from the key-value format string
func (a *HookAuth) GetValues(s string) {
m := map[string]string{}
// make line breaks consistent on Windows platform
s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines
for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2) //nolint: gomnd
// skips non key and value format
if len(v) != 2 { //nolint: gomnd
continue
}
fieldKey := strings.TrimSpace(v[0])
fieldValue := strings.TrimSpace(v[1])
if a.Fields.IsValid(fieldKey) {
m[fieldKey] = fieldValue
}
}
a.Fields.Values = m
}
// SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Cred.Username)
if err != nil && err != errors.ErrNotExist {
return nil, err
}
if u == nil {
// create user with the provided credentials
d := &users.User{
NonAdminEditable: users.NonAdminEditable{
Password: a.Cred.Password,
Locale: a.Settings.UserDefaults.Locale,
ViewMode: a.Settings.UserDefaults.ViewMode,
SingleClick: a.Settings.UserDefaults.SingleClick,
ShowHidden: a.Settings.UserDefaults.ShowHidden,
},
Username: a.Cred.Username,
Permissions: a.Settings.UserDefaults.Permissions,
}
u = a.GetUser(d)
err = a.Users.Save(u, false, false)
if err != nil {
return nil, err
}
return u, nil
}
err = users.CheckPwd(a.Cred.Password, u.Password)
if err != nil {
return nil, err
}
if len(a.Fields.Values) > 1 {
u = a.GetUser(u)
// update user with provided fields
err := a.Users.Update(u, u.Permissions.Admin)
if err != nil {
return nil, err
}
}
return u, nil
}
// GetUser returns a User filled with hook values or provided defaults
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := d.Permissions.Admin
perms := users.Permissions{
Admin: isAdmin,
Modify: isAdmin || d.Permissions.Modify,
Share: isAdmin || d.Permissions.Share,
}
user := users.User{
NonAdminEditable: users.NonAdminEditable{
Password: d.Password,
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: a.Fields.GetString("user.viewMode", d.ViewMode),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
ShowHidden: a.Fields.GetBoolean("user.showHidden", d.ShowHidden),
},
ID: d.ID,
Username: d.Username,
Scopes: d.Scopes,
Permissions: perms,
LockPassword: true,
}
return &user
}
// hookFields is used to access fields from the hook
type hookFields struct {
Values map[string]string
}
// validHookFields contains names of the fields that can be used
var validHookFields = []string{
"hook.action",
"user.scope",
"user.locale",
"user.viewMode",
"user.singleClick",
"user.sorting.by",
"user.sorting.asc",
"user.showHidden",
"user.perm.admin",
"user.perm.modify",
"user.perm.share",
"user.perm.api",
}
// IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields {
if field == val {
return true
}
}
return false
}
// GetString returns the string value or provided default
func (hf *hookFields) GetString(k, dv string) string {
val, ok := hf.Values[k]
if ok {
return val
}
return dv
}
// GetBoolean returns the bool value or provided default
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
val, ok := hf.Values[k]
if ok {
return val == "true"
}
return dv
}
// GetArray returns the array value or provided default
func (hf *hookFields) GetArray(k string, dv []string) []string {
val, ok := hf.Values[k]
if ok && strings.TrimSpace(val) != "" {
return strings.Split(val, " ")
}
return dv
}
-4
View File
@@ -31,10 +31,6 @@ func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error)
if err != nil {
return nil, err
}
err = store.Save(&HookAuth{})
if err != nil {
return nil, err
}
err = store.Save(&NoAuth{})
if err != nil {
return nil, err
+9 -15
View File
@@ -19,6 +19,7 @@ func validateUserInfo() {
logger.Fatalf("could not load users: %v", err)
}
for _, user := range usersList {
changePass := false
updateUser := false
if user.Username == "publicUser" {
settings.ApplyUserDefaults(user)
@@ -36,6 +37,13 @@ func validateUserInfo() {
if updateLoginType(user) {
updateUser = true
}
if user.Username == settings.Config.Auth.AdminUsername && settings.Config.Auth.AdminPassword != "" {
logger.Info("Resetting admin user to default username and password.")
user.Permissions.Admin = true
user.Password = settings.Config.Auth.AdminPassword
updateUser = true
changePass = true
}
if updateUser {
if len(createBackup) == 1 {
logger.Warning("Incompatible user settings detected, creating backup of database before converting.")
@@ -44,26 +52,12 @@ func validateUserInfo() {
logger.Fatalf("Unable to create automatic backup of database due to error: %v", err)
}
}
err := store.Users.Save(user, false, true)
err := store.Users.Save(user, changePass, true)
if err != nil {
logger.Errorf("could not update user: %v", err)
}
}
}
if settings.Config.Auth.ResetAdminOnStart {
logger.Info("Resetting admin user to default username and password.")
adminUser, err := store.Users.Get(1)
if err != nil {
logger.Fatalf("could not load admin user: %v", err)
}
adminUser.Username = settings.Config.Auth.AdminUsername
adminUser.Password = settings.Config.Auth.AdminPassword
adminUser.Permissions.Admin = true
err = store.Users.Save(adminUser, true, true)
if err != nil {
logger.Errorf("could not Save admin user: %v", err)
}
}
}
func updateUserScopes(user *users.User) bool {
+4 -5
View File
@@ -14,11 +14,10 @@ import (
type Auth struct {
TokenExpirationHours int `json:"tokenExpirationHours"` // the number of hours until the token expires. 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".
ResetAdminOnStart bool `json:"resetAdminOnStart"` // if set to true, the admin user will be reset to the default username and password on startup.
TotpSecret string `json:"totpSecret"` // secret used to encrypt TOTP secrets
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
AuthMethods []string `json:"-"`
}
+14 -10
View File
@@ -26,20 +26,22 @@ const (
func Initialize(configFile string) {
err := loadConfigWithDefaults(configFile)
if err != nil {
logger.Error("unable to load config, waiting 5 seconds before exiting...")
time.Sleep(5 * time.Second) // allow sleep time before exiting to give docker/kubernetes time before restarting
logger.Fatal(err.Error())
}
err = ValidateConfig(Config)
if err != nil {
errmsg := "The provided config file failed validation. "
errmsg += "If you are seeing this on a config that worked previeously, "
errmsg += "please review your config for typos and invalid keys which are no longer supported. "
errmsg += "If you are seeing this on a config that worked before, "
errmsg += "then check the latest releases for breaking changes. "
errmsg += "visit https://github.com/gtsteffaniak/filebrowser/wiki/Full-Config-Example for more information."
logger.Error(errmsg)
time.Sleep(5 * time.Second) // allow sleep time before exiting to give docker/kubernetes time before restarting
logger.Fatal(err.Error())
}
setupLogging()
setupAuth()
setupAuth(false)
setupSources(false)
setupUrls()
setupFrontend()
@@ -168,7 +170,10 @@ func setupUrls() {
Config.Integrations.OnlyOffice.InternalUrl = strings.Trim(Config.Integrations.OnlyOffice.InternalUrl, "/")
}
func setupAuth() {
func setupAuth(generate bool) {
if generate {
Config.Auth.AdminPassword = "admin"
}
if Config.Auth.Methods.PasswordAuth.Enabled {
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "password")
}
@@ -342,7 +347,6 @@ func setDefaults() Settings {
},
Auth: Auth{
AdminUsername: "admin",
AdminPassword: "admin",
TokenExpirationHours: 2,
Methods: LoginMethods{
PasswordAuth: PasswordAuthConfig{
@@ -502,9 +506,9 @@ func modifyExcludeInclude(config *Source) {
}
}
normalize(config.Config.Exclude.Files, true)
normalize(config.Config.Exclude.Folders, true)
normalize(config.Config.Include.Files, true)
normalize(config.Config.Include.Folders, true)
normalize(config.Config.Exclude.FolderPaths, true)
normalize(config.Config.Exclude.FilePaths, true)
normalize(config.Config.Include.RootFolders, true)
normalize(config.Config.Include.RootFiles, true)
normalize(config.Config.NeverWatchPaths, true)
}
+1 -1
View File
@@ -175,7 +175,7 @@ func GenerateYaml() {
}
setupLogging()
setupAuth()
setupAuth(true)
setupSources(true)
setupUrls()
setupFrontend()
+23 -15
View File
@@ -78,23 +78,31 @@ type Source struct {
}
type SourceConfig struct {
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in seconds to re-index the source
DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
NeverWatch []string `json:"neverWatchPaths"` // paths to never watch, relative to the source path (eg. "/folder/file.txt")
IgnoreHidden bool `json:"ignoreHidden"` // ignore hidden files and folders.
IgnoreZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
Exclude IndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
Include IndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in seconds to re-index the source
DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
}
type IndexFilter struct {
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 IncludeIndexFilter struct {
RootFolders []string `json:"rootFolders"` // list of root folders to include, relative to the source path (eg. "folder1")
RootFiles []string `json:"rootFiles"` // list of root files to include, relative to the source path (eg. "file1.txt")
}
type ExcludeIndexFilter struct {
Hidden bool `json:"hidden"` // exclude hidden files and folders.
ZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
FilePaths []string `json:"filePaths"` // list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (without source path prefix)
FolderPaths []string `json:"folderPaths"` // (filepath) list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
FileNames []string `json:"fileNames"` // (global) list of file names to include/exclude. Eg. "a.jpg"
FolderNames []string `json:"folderNames"` // (global) list of folder names to include/exclude. Eg. "@eadir" or ".thumbnails"
FileEndsWith []string `json:"fileEndsWith"` // (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
FolderEndsWith []string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
}
type Frontend struct {
-2
View File
@@ -17,8 +17,6 @@ func (s authBackend) Get(t string) (auth.Auther, error) {
auther = &auth.JSONAuth{}
case "proxy":
auther = &auth.ProxyAuth{}
case "hook":
auther = &auth.HookAuth{}
case "noauth":
auther = &auth.NoAuth{}
default:
+5 -1
View File
@@ -89,6 +89,9 @@ func quickSetup(store *Storage) {
user := &users.User{}
settings.ApplyUserDefaults(user)
user.Username = settings.Config.Auth.AdminUsername
if settings.Config.Auth.AdminPassword == "" {
settings.Config.Auth.AdminPassword = "admin"
}
user.Password = settings.Config.Auth.AdminPassword
user.Permissions.Admin = true
user.Scopes = []users.SourceScope{}
@@ -119,7 +122,8 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
}
userInfo.Password = hashpass
}
// must have username or password to create
userInfo.Permissions = settings.Config.UserDefaults.Permissions
// must have username
if userInfo.Username == "" {
return fmt.Errorf("username is required to create a user")
}
+4 -4
View File
@@ -20,12 +20,12 @@ import (
// @Param name query string true "Name of the API key"
// @Param days query string true "Duration of the API key in days"
// @Param permissions query string true "Permissions for the API key (comma-separated)"
// @Success 200 {object} HttpResponse "Token created successfully, resoponse contains json object with token key"
// @Success 200 {object} HttpResponse "Token created successfully, response contains json object with token key"
// @Failure 400 {object} map[string]string "Bad request"
// @Failure 404 {object} map[string]string "Not found"
// @Failure 409 {object} map[string]string "Conflict"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/createApiKey [post]
// @Router /api/auth/token [put]
func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
name := r.URL.Query().Get("name")
durationStr := r.URL.Query().Get("days")
@@ -85,7 +85,7 @@ func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestConte
// @Success 200 {object} HttpResponse "API key deleted successfully"
// @Failure 404 {object} map[string]string "Not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/deleteApiKey [delete]
// @Router /api/auth/token [delete]
func deleteApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
name := r.URL.Query().Get("name")
if !d.user.Permissions.Api {
@@ -127,7 +127,7 @@ type AuthTokenMin struct {
// @Success 200 {object} AuthTokenMin "List of API keys or specific key details"
// @Failure 404 {object} map[string]string "Not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/listApiKeys [get]
// @Router /api/auth/tokens [get]
func listApiKeysHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
key := r.URL.Query().Get("name")
if !d.user.Permissions.Api {
+19 -7
View File
@@ -125,25 +125,37 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
// logoutHandler handles user logout, specifically used for OIDC or proxy users.
// @Summary User Logout
// @Description logs a user out of the application.
// @Description Returns a logout URL for the frontend to redirect to.
// @Tags Auth
// @Success 302 {string} string "Redirect to redirect URL if configured in oidc config."
// @Produce json
// @Success 200 {object} map[string]string "{"logoutUrl": "http://..."}"
// @Router /api/auth/logout [post]
func logoutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
logoutUrl := fmt.Sprintf("%vlogin", config.Server.BaseURL)
if d.user.LoginMethod == "proxy" {
logoutUrl := fmt.Sprintf("%vlogin", config.Server.BaseURL) // Default fallback
if d.user.LoginMethod == users.LoginMethodProxy {
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
if proxyRedirectUrl != "" {
logoutUrl = proxyRedirectUrl
}
} else if d.user.LoginMethod == "oidc" {
} else if d.user.LoginMethod == users.LoginMethodOidc {
oidcRedirectUrl := config.Auth.Methods.OidcAuth.LogoutRedirectUrl
if oidcRedirectUrl != "" {
logoutUrl = oidcRedirectUrl
}
}
http.Redirect(w, r, logoutUrl, http.StatusFound)
return 302, nil
response := map[string]string{
"logoutUrl": logoutUrl,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(response)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
type signupBody struct {
+11
View File
@@ -8,6 +8,17 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)
// getJobsHandler returns job info for the user.
// @Summary Get jobs info
// @Description Returns job info for the user.
// @Tags Jobs
// @Accept json
// @Produce json
// @Param action path string true "Job action"
// @Param target path string true "Job target"
// @Success 200 {object} map[string]indexing.ReducedIndex "Job info"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/jobs/{action}/{target} [get]
func getJobsHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
sources := settings.GetSources(d.user)
reducedIndexes := map[string]indexing.ReducedIndex{}
+22 -6
View File
@@ -29,9 +29,14 @@ type userInfo struct {
Groups []string `json:"groups"`
}
// oidcLoginHandler redirects the user to the OIDC provider's authorization endpoint.
// This function remains largely the same, but includes the 'fb_redirect' parameter
// to redirect the user back to the original page after successful login.
// oidcLoginHandler initiates OIDC login.
// @Summary OIDC login
// @Description Initiates OIDC login flow.
// @Tags OIDC
// @Accept json
// @Produce json
// @Success 302 {string} string "Redirect to OIDC provider"
// @Router /api/auth/oidc/login [get]
func oidcLoginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
oidcCfg := settings.Config.Auth.Methods.OidcAuth
if !oidcCfg.Enabled {
@@ -58,9 +63,17 @@ func oidcLoginHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
return 0, nil
}
// oidcCallbackHandler handles the OIDC callback after the user authenticates with the provider.
// It exchanges the authorization code for tokens, attempts to verify the ID token.
// If ID token verification or essential claims extraction fails, it falls back to the UserInfo endpoint.
// oidcCallbackHandler handles OIDC callback.
// @Summary OIDC callback
// @Description Handles OIDC login callback.
// @Tags OIDC
// @Accept json
// @Produce json
// @Param code query string false "OIDC code"
// @Param state query string false "OIDC state"
// @Success 200 {object} map[string]string "OIDC callback result"
// @Failure 400 {object} map[string]string "Bad request"
// @Router /api/auth/oidc/callback [get]
func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
ctx := r.Context()
oidcCfg := settings.Config.Auth.Methods.OidcAuth
@@ -221,6 +234,9 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
return http.StatusInternalServerError, err
}
if config.Auth.Methods.OidcAuth.CreateUser {
if config.Auth.Methods.OidcAuth.AdminGroup == "" {
isAdmin = config.UserDefaults.Permissions.Admin
}
err = storage.CreateUser(users.User{
LoginMethod: users.LoginMethodOidc,
Username: username,
+2 -4
View File
@@ -29,7 +29,6 @@ import (
// @Produce json
// @Param path query string true "Path to the resource"
// @Param source query string false "Source name for the desired source, default is used if not provided"
// @Param source query string false "Name for the desired source, default is used if not provided"
// @Param content query string false "Include file content if true"
// @Param checksum query string false "Optional checksum validation"
// @Success 200 {object} iteminfo.FileInfo "Resource metadata"
@@ -97,7 +96,6 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Produce json
// @Param path query string true "Path to the resource"
// @Param source query string false "Source name for the desired source, default is used if not provided"
// @Param source query string false "Name for the desired source, default is used if not provided"
// @Success 200 "Resource deleted successfully"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 404 {object} map[string]string "Resource not found"
@@ -156,7 +154,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
// @Tags Resources
// @Accept json
// @Produce json
// @Param path query string true "Destination path where to place the files inside the destination source, a directory must end in / to create a directory"
// @Param path query string true "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory"
// @Param source query string false "Name for the desired filebrowser destination source name, default is used if not provided"
// @Param override query bool false "Override existing file if true"
// @Success 200 "Resource created successfully"
@@ -178,6 +176,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err)
}
}
fmt.Println("resourcePostHandler: source:", source, "path:", path)
if !d.user.Permissions.Modify {
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
}
@@ -229,7 +228,6 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
// @Produce json
// @Param path query string true "Destination path where to place the files inside the destination source"
// @Param source query string false "Source name for the desired source, default is used if not provided"
// @Param source query string false "Name for the desired source, default is used if not provided"
// @Success 200 "Resource updated successfully"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 404 {object} map[string]string "Resource not found"
+4 -5
View File
@@ -102,11 +102,11 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
// @Tags Shares
// @Accept json
// @Produce json
// @Param hash path string true "Hash of the share link to delete"
// @Param hash query string true "Hash of the share link to delete"
// @Success 200 "Share link deleted successfully"
// @Failure 400 {object} map[string]string "Bad request - missing or invalid hash"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/shares/{hash} [delete]
// @Router /api/shares [delete]
func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
hash := r.URL.Query().Get("hash")
@@ -128,9 +128,8 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Tags Shares
// @Accept json
// @Produce json
// @Param body body share.CreateBody true "Share link creation parameters"
// @Param path path string true "Source Path of the files to share"
// @Param source path string true "Source name of the files to share"
// @Param path query string true "Source Path of the files to share"
// @Param source query string true "Source name of the files to share"
// @Success 200 {object} share.Link "Created share link"
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
// @Failure 500 {object} map[string]string "Internal server error"
+8 -8
View File
@@ -22,16 +22,16 @@ type UserRequest struct {
// userGetHandler retrieves a user by ID.
// @Summary Retrieve a user by ID
// @Description Returns a user's details based on their ID.
// @Description Returns a user's details based on their ID, or all users if no id is provided.
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID" or "self"
// @Success 200 {object} users.User "User details"
// @Param id query string false "User ID or 'self'"
// @Success 200 {object} users.User "User details or list of users"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 404 {object} map[string]string "Not Found"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /api/users/{id} [get]
// @Router /api/users [get]
func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
givenUserIdString := r.URL.Query().Get("id")
@@ -100,11 +100,11 @@ func prepForFrontend(u *users.User) {
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param id query string true "User ID"
// @Success 200 "User deleted successfully"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /api/users/{id} [delete]
// @Router /api/users [delete]
func userDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
givenUserIdString := r.URL.Query().Get("id")
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
@@ -176,13 +176,13 @@ func usersPostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param id query string true "User ID"
// @Param data body users.User true "User data to update"
// @Success 200 {object} users.User "Updated user details"
// @Failure 400 {object} map[string]string "Bad Request"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /api/users/{id} [put]
// @Router /api/users [put]
func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
givenUserIdString := r.URL.Query().Get("id")
num, _ := strconv.ParseUint(givenUserIdString, 10, 32)
+91 -45
View File
@@ -46,6 +46,7 @@ type Index struct {
FilesChangedDuringIndexing bool `json:"-"`
mock bool
mu sync.RWMutex
hasIndex bool
}
var (
@@ -109,7 +110,7 @@ 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) {
if idx.shouldSkip(dirInfo.IsDir(), hidden, adjustedPath, dirInfo.Name()) {
return errors.ErrNotIndexed
}
@@ -152,7 +153,7 @@ func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) err
return nil
}
}
dirFileInfo, err2 := idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, quick, recursive,true)
dirFileInfo, err2 := idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, quick, recursive, true)
if err2 != nil {
return err2
}
@@ -186,7 +187,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,false)
response, err = idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, false, false, false)
if err != nil {
return nil, err
}
@@ -227,8 +228,14 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
for _, file := range files {
hidden := isHidden(file, idx.Path+combinedPath)
isDir := iteminfo.IsDirectory(file)
fullCombined := combinedPath + file.Name()
if checkSkip && idx.shouldSkip(isDir, hidden, fullCombined) {
baseName := file.Name()
fullCombined := combinedPath + baseName
if adjustedPath == "/" {
if !idx.shouldInclude(isDir, combinedPath, file.Name()) {
continue
}
}
if checkSkip && idx.shouldSkip(isDir, hidden, fullCombined, baseName) {
continue
}
itemInfo := &iteminfo.ItemInfo{
@@ -238,12 +245,21 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
}
if isDir {
dirPath := combinedPath + file.Name()
if idx.hasIndex && recursive && len(idx.Config.NeverWatchPaths) > 0 {
if slices.Contains(idx.Config.NeverWatchPaths, fullCombined) {
realDirInfo, exists := idx.GetMetadataInfo(dirPath, true)
if exists {
itemInfo.Size = realDirInfo.Size
}
continue
}
}
// skip non-indexable dirs.
if file.Name() == "$RECYCLE.BIN" || file.Name() == "System Volume Information" {
continue
}
dirPath := combinedPath + file.Name()
if recursive {
// clear for garbage collection
file = nil
@@ -271,7 +287,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
}
}
if totalSize == 0 && idx.Config.IgnoreZeroSizeFolders {
if totalSize == 0 && idx.Config.Exclude.ZeroSizeFolders {
return nil, errors.ErrNotIndexed
}
@@ -394,58 +410,88 @@ func isHidden(file os.FileInfo, srcPath string) bool {
return false
}
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined string) bool {
func (idx *Index) shouldInclude(isDir bool, fullCombined, baseName string) bool {
rules := idx.Config.Include
hasRules := false
if len(rules.RootFolders) > 0 {
hasRules = true
for _, p := range rules.RootFolders {
if strings.HasPrefix(fullCombined, p) {
return true
}
}
}
if len(rules.RootFiles) > 0 {
hasRules = true
for _, p := range rules.RootFiles {
if strings.HasPrefix(fullCombined, p) {
return true
}
}
}
if !hasRules {
return true
}
return false
}
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined, baseName string) bool {
if idx.Config.DisableIndexing {
return true
}
// check inclusions first
if isDir && len(idx.Config.Include.Folders) > 0 {
if !slices.Contains(idx.Config.Include.Folders, fullCombined) {
return true
}
}
if !isDir && len(idx.Config.Include.Files) > 0 {
if !slices.Contains(idx.Config.Include.Files, fullCombined) {
return true
}
}
if !isDir && len(idx.Config.Include.FileEndsWith) > 0 {
shouldSkip := true
for _, end := range idx.Config.Include.FileEndsWith {
if strings.HasSuffix(fullCombined, end) {
shouldSkip = false
break
rules := idx.Config.Exclude
if isDir {
if len(rules.FolderPaths) > 0 {
for _, p := range rules.FolderPaths {
if strings.HasPrefix(fullCombined, p) {
return true
}
}
}
if shouldSkip {
if len(rules.FolderNames) > 0 && slices.Contains(rules.FolderNames, baseName) {
return true
}
}
// check exclusions
if isDir && slices.Contains(idx.Config.Exclude.Folders, fullCombined) {
return true
}
if !isDir && slices.Contains(idx.Config.Exclude.Files, fullCombined) {
return true
}
if idx.Config.IgnoreHidden && isHidden {
return true
}
if !isDir && len(idx.Config.Exclude.FileEndsWith) > 0 {
shouldSkip := false
for _, end := range idx.Config.Exclude.FileEndsWith {
if strings.HasSuffix(fullCombined, end) {
shouldSkip = true
break
if len(rules.FolderEndsWith) > 0 {
for _, end := range rules.FolderEndsWith {
if strings.HasSuffix(baseName, end) {
return true
}
}
}
if shouldSkip {
} else {
if len(rules.FilePaths) > 0 {
for _, p := range rules.FilePaths {
if strings.HasPrefix(fullCombined, p) {
return true
}
}
}
if len(rules.FileNames) > 0 && slices.Contains(rules.FileNames, baseName) {
return true
}
if len(rules.FileEndsWith) > 0 {
for _, end := range rules.FileEndsWith {
if strings.HasSuffix(baseName, end) {
return true
}
}
}
// Exclude if parent directory matches FolderPaths
if len(rules.FolderPaths) > 0 {
parent := filepath.Dir(fullCombined)
for _, p := range rules.FolderPaths {
if strings.HasPrefix(parent, p) {
return true
}
}
}
}
if rules.Hidden && isHidden {
return true
}
return false
}
+1
View File
@@ -56,6 +56,7 @@ func (idx *Index) PreScan() {
func (idx *Index) PostScan() {
idx.mu.Lock()
idx.garbageCollection()
idx.hasIndex = true
idx.runningScannerCount--
idx.mu.Unlock()
if idx.runningScannerCount == 0 {
+71 -55
View File
@@ -124,77 +124,93 @@ func TestMakeIndexPathRoot(t *testing.T) {
}
}
func BenchmarkCheckIndexExclude(b *testing.B) {
func TestCheckIndexExclude(t *testing.T) {
tests := []struct {
name string
isDir bool
isHidden bool
fullPath string
baseName string
exclude settings.ExcludeIndexFilter
expect bool
}{
{false, false, "/test/.test"},
{true, false, "/test/.test"},
{true, true, "/test/.test"},
{false, false, "/test/filepath"},
{false, true, "/test/filepath"},
{true, true, "/test/filepath"},
// FilePaths exclusion (prefix match)
{"FilePaths exclusion (file)", false, false, "/test/filepath/child.txt", "child.txt", settings.ExcludeIndexFilter{FilePaths: []string{"/test/filepath"}}, true},
{"FilePaths exclusion (folder)", true, false, "/test/folderpath/sub", "sub", settings.ExcludeIndexFilter{FolderPaths: []string{"/test/folderpath"}}, true},
// FileNames exclusion (exact match)
{"FileNames exclusion (file)", false, false, "/test/abc.txt", "abc.txt", settings.ExcludeIndexFilter{FileNames: []string{"abc.txt"}}, true},
{"FolderNames exclusion (folder)", true, false, "/test/.thumbnails", ".thumbnails", settings.ExcludeIndexFilter{FolderNames: []string{".thumbnails"}}, true},
// FileEndsWith exclusion (suffix match)
{"FileEndsWith exclusion (file)", false, false, "/test/archive.zip", "archive.zip", settings.ExcludeIndexFilter{FileEndsWith: []string{".zip"}}, true},
// FolderEndsWith exclusion (suffix match)
{"FolderEndsWith exclusion (folder)", true, false, "/test/special_folder", "special_folder", settings.ExcludeIndexFilter{FolderEndsWith: []string{"_folder"}}, true},
// Negative cases (should not skip)
{"FilePaths not excluded (file)", false, false, "/test/otherfile", "otherfile", settings.ExcludeIndexFilter{FilePaths: []string{"/test/filepath"}}, false},
{"FileNames not excluded (file)", false, false, "/test/other.txt", "other.txt", settings.ExcludeIndexFilter{FileNames: []string{"abc.txt"}}, false},
{"FolderNames not excluded (folder)", true, false, "/test/otherfolder", "otherfolder", settings.ExcludeIndexFilter{FolderNames: []string{".thumbnails"}}, false},
{"FileEndsWith not excluded (file)", false, false, "/test/file.tar", "file.tar", settings.ExcludeIndexFilter{FileEndsWith: []string{".zip"}}, false},
{"FolderEndsWith not excluded (folder)", true, false, "/test/normal", "normal", settings.ExcludeIndexFilter{FolderEndsWith: []string{"_folder"}}, false},
}
b.ResetTimer()
b.ReportAllocs()
idx := Index{
Source: settings.Source{
Name: "files",
Config: settings.SourceConfig{
IgnoreHidden: true,
Exclude: settings.IndexFilter{
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
Folders: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
FileEndsWith: []string{".zip", ".tar", ".jpeg"},
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
idx := Index{
Source: settings.Source{
Name: "files",
Config: settings.SourceConfig{
Exclude: tc.exclude,
},
},
},
},
}
result := idx.shouldSkip(tc.isDir, tc.isHidden, tc.fullPath, tc.baseName)
if result != tc.expect {
t.Errorf("shouldSkip(%v, %v, %q, %q) = %v; want %v", tc.isDir, tc.isHidden, tc.fullPath, tc.baseName, result, tc.expect)
}
})
}
for i := 0; i < b.N; i++ {
for _, v := range tests {
idx.shouldSkip(v.isDir, v.isHidden, v.fullPath)
}
}
}
func BenchmarkCheckIndexConditionsInclude(b *testing.B) {
func TestCheckIndexInclude(t *testing.T) {
tests := []struct {
name string
isDir bool
isHidden bool
fullPath string
baseName string
include settings.IncludeIndexFilter
expect bool
}{
{false, false, "/test/.test"},
{true, false, "/test/.test"},
{true, true, "/test/.test"},
{false, false, "/test/filepath"},
{false, true, "/test/filepath"},
{true, true, "/test/filepath"},
// RootFolders inclusion (prefix match)
{"RootFolders include (folder)", true, "/folder1", "folder1", settings.IncludeIndexFilter{RootFolders: []string{"/folder1"}}, true},
{"RootFolders include (subfolder)", true, "/folder1/sub", "sub", settings.IncludeIndexFilter{RootFolders: []string{"/folder1"}}, true},
{"RootFolders not include (folder)", true, "/otherfolder", "otherfolder", settings.IncludeIndexFilter{RootFolders: []string{"/folder1"}}, false},
// RootFiles inclusion (prefix match)
{"RootFiles include (file)", false, "/file1.txt", "file1.txt", settings.IncludeIndexFilter{RootFiles: []string{"/file1.txt"}}, true},
{"RootFiles include (file in subfolder)", false, "/folder1/file1.txt", "file1.txt", settings.IncludeIndexFilter{RootFiles: []string{"/folder1/file1.txt"}}, true},
{"RootFiles not include (file)", false, "/otherfile.txt", "otherfile.txt", settings.IncludeIndexFilter{RootFiles: []string{"/file1.txt"}}, false},
// No rules: should include everything
{"No rules include (file)", false, "/anyfile.txt", "anyfile.txt", settings.IncludeIndexFilter{}, true},
{"No rules include (folder)", true, "/anyfolder", "anyfolder", settings.IncludeIndexFilter{}, true},
}
b.ResetTimer()
b.ReportAllocs()
idx2 := Index{
Source: settings.Source{
Name: "files",
Config: settings.SourceConfig{
IgnoreHidden: true,
Include: settings.IndexFilter{
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
Folders: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
FileEndsWith: []string{".zip", ".tar", ".jpeg"},
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
idx := Index{
Source: settings.Source{
Name: "files",
Config: settings.SourceConfig{
Include: tc.include,
},
},
},
},
}
result := idx.shouldInclude(tc.isDir, tc.fullPath, tc.baseName)
if result != tc.expect {
t.Errorf("shouldInclude(%v, %q, %q) = %v; want %v", tc.isDir, tc.fullPath, tc.baseName, result, tc.expect)
}
})
}
for i := 0; i < b.N; i++ {
for _, v := range tests {
idx2.shouldSkip(v.isDir, v.isHidden, v.fullPath)
}
}
}
+1
View File
@@ -74,6 +74,7 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
}
settings.Config.Server.MuPdfAvailable = docEnabled()
logger.Infof("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
return &Service{
sem: make(chan struct{}, concurrencyLimit),
ffmpegPath: ffmpegMainPath,
+379 -211
View File
@@ -17,14 +17,92 @@ const docTemplate = `{
"paths": {
"/api/auth/logout": {
"post": {
"description": "logs a user out of the application.",
"description": "Returns a logout URL for the frontend to redirect to.",
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "User Logout",
"responses": {
"200": {
"description": "{\"logoutUrl\": \"http://...\"}",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/auth/oidc/callback": {
"get": {
"description": "Handles OIDC login callback.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"OIDC"
],
"summary": "OIDC callback",
"parameters": [
{
"type": "string",
"description": "OIDC code",
"name": "code",
"in": "query"
},
{
"type": "string",
"description": "OIDC state",
"name": "state",
"in": "query"
}
],
"responses": {
"200": {
"description": "OIDC callback result",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/auth/oidc/login": {
"get": {
"description": "Initiates OIDC login flow.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"OIDC"
],
"summary": "OIDC login",
"responses": {
"302": {
"description": "Redirect to redirect URL if configured in oidc config.",
"description": "Redirect to OIDC provider",
"schema": {
"type": "string"
}
@@ -118,8 +196,8 @@ const docTemplate = `{
}
}
},
"/api/createApiKey": {
"post": {
"/api/auth/token": {
"put": {
"description": "Create an API key with specified name, duration, and permissions.",
"consumes": [
"application/json"
@@ -156,7 +234,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "Token created successfully, resoponse contains json object with token key",
"description": "Token created successfully, response contains json object with token key",
"schema": {
"$ref": "#/definitions/http.HttpResponse"
}
@@ -198,9 +276,7 @@ const docTemplate = `{
}
}
}
}
},
"/api/deleteApiKey": {
},
"delete": {
"description": "Delete an API key with specified name.",
"consumes": [
@@ -250,7 +326,7 @@ const docTemplate = `{
}
}
},
"/api/listApiKeys": {
"/api/auth/tokens": {
"get": {
"description": "List all API keys or retrieve details for a specific key.",
"consumes": [
@@ -299,6 +375,57 @@ const docTemplate = `{
}
}
},
"/api/jobs/{action}/{target}": {
"get": {
"description": "Returns job info for the user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Jobs"
],
"summary": "Get jobs info",
"parameters": [
{
"type": "string",
"description": "Job action",
"name": "action",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Job target",
"name": "target",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Job info",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/indexing.ReducedIndex"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/login": {
"post": {
"description": "Authenticate a user with a username and password.",
@@ -578,12 +705,6 @@ const docTemplate = `{
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Include file content if true",
@@ -649,12 +770,6 @@ const docTemplate = `{
"description": "Source name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
}
],
"responses": {
@@ -714,7 +829,7 @@ const docTemplate = `{
"parameters": [
{
"type": "string",
"description": "Destination path where to place the files inside the destination source, a directory must end in / to create a directory",
"description": "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory",
"name": "path",
"in": "query",
"required": true
@@ -799,12 +914,6 @@ const docTemplate = `{
"description": "Source name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
}
],
"responses": {
@@ -1115,27 +1224,18 @@ const docTemplate = `{
],
"summary": "Create a share link",
"parameters": [
{
"description": "Share link creation parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/share.CreateBody"
}
},
{
"type": "string",
"description": "Source Path of the files to share",
"name": "path",
"in": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name of the files to share",
"name": "source",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1165,9 +1265,7 @@ const docTemplate = `{
}
}
}
}
},
"/api/shares/{hash}": {
},
"delete": {
"description": "Deletes a share link specified by its hash.",
"consumes": [
@@ -1185,7 +1283,7 @@ const docTemplate = `{
"type": "string",
"description": "Hash of the share link to delete",
"name": "hash",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1285,6 +1383,128 @@ const docTemplate = `{
}
},
"/api/users": {
"get": {
"description": "Returns a user's details based on their ID, or all users if no id is provided.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Retrieve a user by ID",
"parameters": [
{
"type": "string",
"description": "User ID or 'self'",
"name": "id",
"in": "query"
}
],
"responses": {
"200": {
"description": "User details or list of users",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"put": {
"description": "Updates the details of a user identified by ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update a user's details",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "query",
"required": true
},
{
"description": "User data to update",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/users.User"
}
}
],
"responses": {
"200": {
"description": "Updated user details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"post": {
"description": "Adds a new user to the system.",
"consumes": [
@@ -1334,131 +1554,6 @@ const docTemplate = `{
}
}
}
}
},
"/api/users/{id}": {
"get": {
"description": "Returns a user's details based on their ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Retrieve a user by ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "User details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"put": {
"description": "Updates the details of a user identified by ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update a user's details",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "User data to update",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/users.User"
}
}
],
"responses": {
"200": {
"description": "Updated user details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes a user identified by their ID.",
@@ -1474,10 +1569,10 @@ const docTemplate = `{
"summary": "Delete a user by ID",
"parameters": [
{
"type": "integer",
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1656,6 +1751,57 @@ const docTemplate = `{
}
}
},
"indexing.IndexStatus": {
"type": "string",
"enum": [
"ready",
"indexing",
"unavailable"
],
"x-enum-varnames": [
"READY",
"INDEXING",
"UNAVAILABLE"
]
},
"indexing.ReducedIndex": {
"type": "object",
"properties": {
"assessment": {
"type": "string"
},
"fullScanDurationSeconds": {
"type": "integer"
},
"lastIndexedUnixTime": {
"type": "integer"
},
"name": {
"type": "string"
},
"numDeleted": {
"type": "integer"
},
"numDirs": {
"type": "integer"
},
"numFiles": {
"type": "integer"
},
"quickScanDurationSeconds": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/indexing.IndexStatus"
},
"total": {
"type": "integer"
},
"used": {
"type": "integer"
}
}
},
"indexing.SearchResult": {
"type": "object",
"properties": {
@@ -1756,10 +1902,6 @@ const docTemplate = `{
"methods": {
"$ref": "#/definitions/settings.LoginMethods"
},
"resetAdminOnStart": {
"description": "if set to true, the admin user will be reset to the default username and password on startup.",
"type": "boolean"
},
"tokenExpirationHours": {
"description": "the number of hours until the token expires. Default is 2 hours.",
"type": "integer"
@@ -1770,6 +1912,61 @@ const docTemplate = `{
}
}
},
"settings.ExcludeIndexFilter": {
"type": "object",
"properties": {
"fileEndsWith": {
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
"type": "array",
"items": {
"type": "string"
}
},
"fileNames": {
"description": "(global) list of file names to include/exclude. Eg. \"a.jpg\"",
"type": "array",
"items": {
"type": "string"
}
},
"filePaths": {
"description": "list of filepaths Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (without source path prefix)",
"type": "array",
"items": {
"type": "string"
}
},
"folderEndsWith": {
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
"type": "array",
"items": {
"type": "string"
}
},
"folderNames": {
"description": "(global) list of folder names to include/exclude. Eg. \"@eadir\" or \".thumbnails\"",
"type": "array",
"items": {
"type": "string"
}
},
"folderPaths": {
"description": "(filepath) 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"
}
},
"hidden": {
"description": "exclude hidden files and folders.",
"type": "boolean"
},
"ignoreZeroSizeFolders": {
"description": "ignore folders with 0 size",
"type": "boolean"
}
}
},
"settings.ExternalLink": {
"type": "object",
"required": [
@@ -1814,25 +2011,18 @@ const docTemplate = `{
}
}
},
"settings.IndexFilter": {
"settings.IncludeIndexFilter": {
"type": "object",
"properties": {
"fileEndsWith": {
"description": "list of file names to include/exclude. Eg. \"a.jpg\"",
"rootFiles": {
"description": "list of root files to include, relative to the source path (eg. \"file1.txt\")",
"type": "array",
"items": {
"type": "string"
}
},
"files": {
"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": "list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
"rootFolders": {
"description": "list of root folders to include, relative to the source path (eg. \"folder1\")",
"type": "array",
"items": {
"type": "string"
@@ -2182,23 +2372,15 @@ const docTemplate = `{
"description": "exclude files and folders from indexing, if include is not set",
"allOf": [
{
"$ref": "#/definitions/settings.IndexFilter"
"$ref": "#/definitions/settings.ExcludeIndexFilter"
}
]
},
"ignoreHidden": {
"description": "ignore hidden files and folders.",
"type": "boolean"
},
"ignoreZeroSizeFolders": {
"description": "ignore folders with 0 size",
"type": "boolean"
},
"include": {
"description": "include files and folders from indexing, if exclude is not set",
"allOf": [
{
"$ref": "#/definitions/settings.IndexFilter"
"$ref": "#/definitions/settings.IncludeIndexFilter"
}
]
},
@@ -2211,7 +2393,7 @@ const docTemplate = `{
"type": "integer"
},
"neverWatchPaths": {
"description": "paths to never watch, relative to the source path (eg. \"/folder/file.txt\")",
"description": "paths that get initially once. Useful for folders that rarely change contents (without source path prefix)",
"type": "array",
"items": {
"type": "string"
@@ -2294,20 +2476,6 @@ const docTemplate = `{
}
}
},
"share.CreateBody": {
"type": "object",
"properties": {
"expires": {
"type": "string"
},
"password": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"share.Link": {
"type": "object",
"properties": {
+379 -211
View File
@@ -6,14 +6,92 @@
"paths": {
"/api/auth/logout": {
"post": {
"description": "logs a user out of the application.",
"description": "Returns a logout URL for the frontend to redirect to.",
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "User Logout",
"responses": {
"200": {
"description": "{\"logoutUrl\": \"http://...\"}",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/auth/oidc/callback": {
"get": {
"description": "Handles OIDC login callback.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"OIDC"
],
"summary": "OIDC callback",
"parameters": [
{
"type": "string",
"description": "OIDC code",
"name": "code",
"in": "query"
},
{
"type": "string",
"description": "OIDC state",
"name": "state",
"in": "query"
}
],
"responses": {
"200": {
"description": "OIDC callback result",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/auth/oidc/login": {
"get": {
"description": "Initiates OIDC login flow.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"OIDC"
],
"summary": "OIDC login",
"responses": {
"302": {
"description": "Redirect to redirect URL if configured in oidc config.",
"description": "Redirect to OIDC provider",
"schema": {
"type": "string"
}
@@ -107,8 +185,8 @@
}
}
},
"/api/createApiKey": {
"post": {
"/api/auth/token": {
"put": {
"description": "Create an API key with specified name, duration, and permissions.",
"consumes": [
"application/json"
@@ -145,7 +223,7 @@
],
"responses": {
"200": {
"description": "Token created successfully, resoponse contains json object with token key",
"description": "Token created successfully, response contains json object with token key",
"schema": {
"$ref": "#/definitions/http.HttpResponse"
}
@@ -187,9 +265,7 @@
}
}
}
}
},
"/api/deleteApiKey": {
},
"delete": {
"description": "Delete an API key with specified name.",
"consumes": [
@@ -239,7 +315,7 @@
}
}
},
"/api/listApiKeys": {
"/api/auth/tokens": {
"get": {
"description": "List all API keys or retrieve details for a specific key.",
"consumes": [
@@ -288,6 +364,57 @@
}
}
},
"/api/jobs/{action}/{target}": {
"get": {
"description": "Returns job info for the user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Jobs"
],
"summary": "Get jobs info",
"parameters": [
{
"type": "string",
"description": "Job action",
"name": "action",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Job target",
"name": "target",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Job info",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/indexing.ReducedIndex"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/login": {
"post": {
"description": "Authenticate a user with a username and password.",
@@ -567,12 +694,6 @@
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Include file content if true",
@@ -638,12 +759,6 @@
"description": "Source name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
}
],
"responses": {
@@ -703,7 +818,7 @@
"parameters": [
{
"type": "string",
"description": "Destination path where to place the files inside the destination source, a directory must end in / to create a directory",
"description": "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory",
"name": "path",
"in": "query",
"required": true
@@ -788,12 +903,6 @@
"description": "Source name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
},
{
"type": "string",
"description": "Name for the desired source, default is used if not provided",
"name": "source",
"in": "query"
}
],
"responses": {
@@ -1104,27 +1213,18 @@
],
"summary": "Create a share link",
"parameters": [
{
"description": "Share link creation parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/share.CreateBody"
}
},
{
"type": "string",
"description": "Source Path of the files to share",
"name": "path",
"in": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name of the files to share",
"name": "source",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1154,9 +1254,7 @@
}
}
}
}
},
"/api/shares/{hash}": {
},
"delete": {
"description": "Deletes a share link specified by its hash.",
"consumes": [
@@ -1174,7 +1272,7 @@
"type": "string",
"description": "Hash of the share link to delete",
"name": "hash",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1274,6 +1372,128 @@
}
},
"/api/users": {
"get": {
"description": "Returns a user's details based on their ID, or all users if no id is provided.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Retrieve a user by ID",
"parameters": [
{
"type": "string",
"description": "User ID or 'self'",
"name": "id",
"in": "query"
}
],
"responses": {
"200": {
"description": "User details or list of users",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"put": {
"description": "Updates the details of a user identified by ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update a user's details",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "query",
"required": true
},
{
"description": "User data to update",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/users.User"
}
}
],
"responses": {
"200": {
"description": "Updated user details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"post": {
"description": "Adds a new user to the system.",
"consumes": [
@@ -1323,131 +1543,6 @@
}
}
}
}
},
"/api/users/{id}": {
"get": {
"description": "Returns a user's details based on their ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Retrieve a user by ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "User details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"put": {
"description": "Updates the details of a user identified by ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update a user's details",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "User data to update",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/users.User"
}
}
],
"responses": {
"200": {
"description": "Updated user details",
"schema": {
"$ref": "#/definitions/users.User"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes a user identified by their ID.",
@@ -1463,10 +1558,10 @@
"summary": "Delete a user by ID",
"parameters": [
{
"type": "integer",
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"in": "query",
"required": true
}
],
@@ -1645,6 +1740,57 @@
}
}
},
"indexing.IndexStatus": {
"type": "string",
"enum": [
"ready",
"indexing",
"unavailable"
],
"x-enum-varnames": [
"READY",
"INDEXING",
"UNAVAILABLE"
]
},
"indexing.ReducedIndex": {
"type": "object",
"properties": {
"assessment": {
"type": "string"
},
"fullScanDurationSeconds": {
"type": "integer"
},
"lastIndexedUnixTime": {
"type": "integer"
},
"name": {
"type": "string"
},
"numDeleted": {
"type": "integer"
},
"numDirs": {
"type": "integer"
},
"numFiles": {
"type": "integer"
},
"quickScanDurationSeconds": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/indexing.IndexStatus"
},
"total": {
"type": "integer"
},
"used": {
"type": "integer"
}
}
},
"indexing.SearchResult": {
"type": "object",
"properties": {
@@ -1745,10 +1891,6 @@
"methods": {
"$ref": "#/definitions/settings.LoginMethods"
},
"resetAdminOnStart": {
"description": "if set to true, the admin user will be reset to the default username and password on startup.",
"type": "boolean"
},
"tokenExpirationHours": {
"description": "the number of hours until the token expires. Default is 2 hours.",
"type": "integer"
@@ -1759,6 +1901,61 @@
}
}
},
"settings.ExcludeIndexFilter": {
"type": "object",
"properties": {
"fileEndsWith": {
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
"type": "array",
"items": {
"type": "string"
}
},
"fileNames": {
"description": "(global) list of file names to include/exclude. Eg. \"a.jpg\"",
"type": "array",
"items": {
"type": "string"
}
},
"filePaths": {
"description": "list of filepaths Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (without source path prefix)",
"type": "array",
"items": {
"type": "string"
}
},
"folderEndsWith": {
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
"type": "array",
"items": {
"type": "string"
}
},
"folderNames": {
"description": "(global) list of folder names to include/exclude. Eg. \"@eadir\" or \".thumbnails\"",
"type": "array",
"items": {
"type": "string"
}
},
"folderPaths": {
"description": "(filepath) 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"
}
},
"hidden": {
"description": "exclude hidden files and folders.",
"type": "boolean"
},
"ignoreZeroSizeFolders": {
"description": "ignore folders with 0 size",
"type": "boolean"
}
}
},
"settings.ExternalLink": {
"type": "object",
"required": [
@@ -1803,25 +2000,18 @@
}
}
},
"settings.IndexFilter": {
"settings.IncludeIndexFilter": {
"type": "object",
"properties": {
"fileEndsWith": {
"description": "list of file names to include/exclude. Eg. \"a.jpg\"",
"rootFiles": {
"description": "list of root files to include, relative to the source path (eg. \"file1.txt\")",
"type": "array",
"items": {
"type": "string"
}
},
"files": {
"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": "list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
"rootFolders": {
"description": "list of root folders to include, relative to the source path (eg. \"folder1\")",
"type": "array",
"items": {
"type": "string"
@@ -2171,23 +2361,15 @@
"description": "exclude files and folders from indexing, if include is not set",
"allOf": [
{
"$ref": "#/definitions/settings.IndexFilter"
"$ref": "#/definitions/settings.ExcludeIndexFilter"
}
]
},
"ignoreHidden": {
"description": "ignore hidden files and folders.",
"type": "boolean"
},
"ignoreZeroSizeFolders": {
"description": "ignore folders with 0 size",
"type": "boolean"
},
"include": {
"description": "include files and folders from indexing, if exclude is not set",
"allOf": [
{
"$ref": "#/definitions/settings.IndexFilter"
"$ref": "#/definitions/settings.IncludeIndexFilter"
}
]
},
@@ -2200,7 +2382,7 @@
"type": "integer"
},
"neverWatchPaths": {
"description": "paths to never watch, relative to the source path (eg. \"/folder/file.txt\")",
"description": "paths that get initially once. Useful for folders that rarely change contents (without source path prefix)",
"type": "array",
"items": {
"type": "string"
@@ -2283,20 +2465,6 @@
}
}
},
"share.CreateBody": {
"type": "object",
"properties": {
"expires": {
"type": "string"
},
"password": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"share.Link": {
"type": "object",
"properties": {
+341 -221
View File
@@ -28,6 +28,41 @@ definitions:
username:
type: string
type: object
indexing.IndexStatus:
enum:
- ready
- indexing
- unavailable
type: string
x-enum-varnames:
- READY
- INDEXING
- UNAVAILABLE
indexing.ReducedIndex:
properties:
assessment:
type: string
fullScanDurationSeconds:
type: integer
lastIndexedUnixTime:
type: integer
name:
type: string
numDeleted:
type: integer
numDirs:
type: integer
numFiles:
type: integer
quickScanDurationSeconds:
type: integer
status:
$ref: '#/definitions/indexing.IndexStatus'
total:
type: integer
used:
type: integer
type: object
indexing.SearchResult:
properties:
path:
@@ -100,10 +135,6 @@ definitions:
type: string
methods:
$ref: '#/definitions/settings.LoginMethods'
resetAdminOnStart:
description: if set to true, the admin user will be reset to the default username
and password on startup.
type: boolean
tokenExpirationHours:
description: the number of hours until the token expires. Default is 2 hours.
type: integer
@@ -111,6 +142,51 @@ definitions:
description: secret used to encrypt TOTP secrets
type: string
type: object
settings.ExcludeIndexFilter:
properties:
fileEndsWith:
description: (global) exclude files that end with these suffixes. Eg. ".jpg"
or ".txt"
items:
type: string
type: array
fileNames:
description: (global) list of file names to include/exclude. Eg. "a.jpg"
items:
type: string
type: array
filePaths:
description: list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt"
(without source path prefix)
items:
type: string
type: array
folderEndsWith:
description: (global) exclude folders that end with these suffixes. Eg. ".thumbnails"
or ".git"
items:
type: string
type: array
folderNames:
description: (global) list of folder names to include/exclude. Eg. "@eadir"
or ".thumbnails"
items:
type: string
type: array
folderPaths:
description: (filepath) 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
hidden:
description: exclude hidden files and folders.
type: boolean
ignoreZeroSizeFolders:
description: ignore folders with 0 size
type: boolean
type: object
settings.ExternalLink:
properties:
text:
@@ -142,23 +218,17 @@ definitions:
description: display name
type: string
type: object
settings.IndexFilter:
settings.IncludeIndexFilter:
properties:
fileEndsWith:
description: list of file names to include/exclude. Eg. "a.jpg"
rootFiles:
description: list of root files to include, relative to the source path (eg.
"file1.txt")
items:
type: string
type: array
files:
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: list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder"
(do not include source path, just the subpaths from the source path)
rootFolders:
description: list of root folders to include, relative to the source path
(eg. "folder1")
items:
type: string
type: array
@@ -418,17 +488,11 @@ definitions:
type: boolean
exclude:
allOf:
- $ref: '#/definitions/settings.IndexFilter'
- $ref: '#/definitions/settings.ExcludeIndexFilter'
description: exclude files and folders from indexing, if include is not set
ignoreHidden:
description: ignore hidden files and folders.
type: boolean
ignoreZeroSizeFolders:
description: ignore folders with 0 size
type: boolean
include:
allOf:
- $ref: '#/definitions/settings.IndexFilter'
- $ref: '#/definitions/settings.IncludeIndexFilter'
description: include files and folders from indexing, if exclude is not set
indexingIntervalMinutes:
description: optional manual overide interval in seconds to re-index the source
@@ -438,7 +502,8 @@ definitions:
not supported
type: integer
neverWatchPaths:
description: paths to never watch, relative to the source path (eg. "/folder/file.txt")
description: paths that get initially once. Useful for folders that rarely
change contents (without source path prefix)
items:
type: string
type: array
@@ -504,15 +569,6 @@ definitions:
description: 'view mode to use: eg. normal, list, grid, or compact'
type: string
type: object
share.CreateBody:
properties:
expires:
type: string
password:
type: string
unit:
type: string
type: object
share.Link:
properties:
expire:
@@ -705,15 +761,66 @@ info:
paths:
/api/auth/logout:
post:
description: logs a user out of the application.
description: Returns a logout URL for the frontend to redirect to.
produces:
- application/json
responses:
"302":
description: Redirect to redirect URL if configured in oidc config.
"200":
description: '{"logoutUrl": "http://..."}'
schema:
type: string
additionalProperties:
type: string
type: object
summary: User Logout
tags:
- Auth
/api/auth/oidc/callback:
get:
consumes:
- application/json
description: Handles OIDC login callback.
parameters:
- description: OIDC code
in: query
name: code
type: string
- description: OIDC state
in: query
name: state
type: string
produces:
- application/json
responses:
"200":
description: OIDC callback result
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad request
schema:
additionalProperties:
type: string
type: object
summary: OIDC callback
tags:
- OIDC
/api/auth/oidc/login:
get:
consumes:
- application/json
description: Initiates OIDC login flow.
produces:
- application/json
responses:
"302":
description: Redirect to OIDC provider
schema:
type: string
summary: OIDC login
tags:
- OIDC
/api/auth/otp/generate:
post:
consumes:
@@ -768,63 +875,7 @@ paths:
summary: Verify OTP
tags:
- OTP
/api/createApiKey:
post:
consumes:
- application/json
description: Create an API key with specified name, duration, and permissions.
parameters:
- description: Name of the API key
in: query
name: name
required: true
type: string
- description: Duration of the API key in days
in: query
name: days
required: true
type: string
- description: Permissions for the API key (comma-separated)
in: query
name: permissions
required: true
type: string
produces:
- application/json
responses:
"200":
description: Token created successfully, resoponse contains json object
with token key
schema:
$ref: '#/definitions/http.HttpResponse'
"400":
description: Bad request
schema:
additionalProperties:
type: string
type: object
"404":
description: Not found
schema:
additionalProperties:
type: string
type: object
"409":
description: Conflict
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Create API key
tags:
- API Keys
/api/deleteApiKey:
/api/auth/token:
delete:
consumes:
- application/json
@@ -857,7 +908,62 @@ paths:
summary: Delete API key
tags:
- API Keys
/api/listApiKeys:
put:
consumes:
- application/json
description: Create an API key with specified name, duration, and permissions.
parameters:
- description: Name of the API key
in: query
name: name
required: true
type: string
- description: Duration of the API key in days
in: query
name: days
required: true
type: string
- description: Permissions for the API key (comma-separated)
in: query
name: permissions
required: true
type: string
produces:
- application/json
responses:
"200":
description: Token created successfully, response contains json object with
token key
schema:
$ref: '#/definitions/http.HttpResponse'
"400":
description: Bad request
schema:
additionalProperties:
type: string
type: object
"404":
description: Not found
schema:
additionalProperties:
type: string
type: object
"409":
description: Conflict
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Create API key
tags:
- API Keys
/api/auth/tokens:
get:
consumes:
- application/json
@@ -889,6 +995,40 @@ paths:
summary: List API keys
tags:
- API Keys
/api/jobs/{action}/{target}:
get:
consumes:
- application/json
description: Returns job info for the user.
parameters:
- description: Job action
in: path
name: action
required: true
type: string
- description: Job target
in: path
name: target
required: true
type: string
produces:
- application/json
responses:
"200":
description: Job info
schema:
additionalProperties:
$ref: '#/definitions/indexing.ReducedIndex'
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Get jobs info
tags:
- Jobs
/api/login:
post:
consumes:
@@ -1076,10 +1216,6 @@ paths:
in: query
name: source
type: string
- description: Name for the desired source, default is used if not provided
in: query
name: source
type: string
produces:
- application/json
responses:
@@ -1121,10 +1257,6 @@ paths:
in: query
name: source
type: string
- description: Name for the desired source, default is used if not provided
in: query
name: source
type: string
- description: Include file content if true
in: query
name: content
@@ -1221,8 +1353,8 @@ paths:
description: Creates a new resource or uploads a file at the specified path.
Supports file uploads and directory creation.
parameters:
- description: Destination path where to place the files inside the destination
source, a directory must end in / to create a directory
- description: url encoded destination path where to place the files inside
the destination source, a directory must end in / to create a directory
in: query
name: path
required: true
@@ -1283,10 +1415,6 @@ paths:
in: query
name: source
type: string
- description: Name for the desired source, default is used if not provided
in: query
name: source
type: string
produces:
- application/json
responses:
@@ -1416,6 +1544,36 @@ paths:
tags:
- Shares
/api/shares:
delete:
consumes:
- application/json
description: Deletes a share link specified by its hash.
parameters:
- description: Hash of the share link to delete
in: query
name: hash
required: true
type: string
produces:
- application/json
responses:
"200":
description: Share link deleted successfully
"400":
description: Bad request - missing or invalid hash
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Delete a share link
tags:
- Shares
get:
consumes:
- application/json
@@ -1445,19 +1603,13 @@ paths:
description: Creates a new share link with an optional expiration time and password
protection.
parameters:
- description: Share link creation parameters
in: body
name: body
required: true
schema:
$ref: '#/definitions/share.CreateBody'
- description: Source Path of the files to share
in: path
in: query
name: path
required: true
type: string
- description: Source name of the files to share
in: path
in: query
name: source
required: true
type: string
@@ -1483,37 +1635,6 @@ paths:
summary: Create a share link
tags:
- Shares
/api/shares/{hash}:
delete:
consumes:
- application/json
description: Deletes a share link specified by its hash.
parameters:
- description: Hash of the share link to delete
in: path
name: hash
required: true
type: string
produces:
- application/json
responses:
"200":
description: Share link deleted successfully
"400":
description: Bad request - missing or invalid hash
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Delete a share link
tags:
- Shares
/api/signup:
post:
consumes:
@@ -1561,6 +1682,74 @@ paths:
tags:
- Auth
/api/users:
delete:
consumes:
- application/json
description: Deletes a user identified by their ID.
parameters:
- description: User ID
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: User deleted successfully
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Delete a user by ID
tags:
- Users
get:
consumes:
- application/json
description: Returns a user's details based on their ID, or all users if no
id is provided.
parameters:
- description: User ID or 'self'
in: query
name: id
type: string
produces:
- application/json
responses:
"200":
description: User details or list of users
schema:
$ref: '#/definitions/users.User'
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Retrieve a user by ID
tags:
- Users
post:
consumes:
- application/json
@@ -1594,85 +1783,16 @@ paths:
summary: Create a new user
tags:
- Users
/api/users/{id}:
delete:
consumes:
- application/json
description: Deletes a user identified by their ID.
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: User deleted successfully
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Delete a user by ID
tags:
- Users
get:
consumes:
- application/json
description: Returns a user's details based on their ID.
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: User details
schema:
$ref: '#/definitions/users.User'
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Retrieve a user by ID
tags:
- Users
put:
consumes:
- application/json
description: Updates the details of a user identified by ID.
parameters:
- description: User ID
in: path
in: query
name: id
required: true
type: integer
type: string
- description: User data to update
in: body
name: data
+10 -9
View File
@@ -1,5 +1,5 @@
server:
numImageProcessors: 16 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
numImageProcessors: 8 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
socket: "" # socket to listen on
tlsKey: "" # path to TLS key
tlsCert: "" # path to TLS cert
@@ -25,16 +25,18 @@ server:
disableIndexing: false # disable the indexing of this source
maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported
neverWatchPaths: []
ignoreHidden: false # ignore hidden files and folders.
ignoreZeroSizeFolders: false # ignore folders with 0 size
exclude: # exclude files and folders from indexing, if include is not set
files: []
folders: []
hidden: false # exclude hidden files and folders.
ignoreZeroSizeFolders: false # ignore folders with 0 size
filePaths: []
folderPaths: []
fileNames: []
folderNames: []
fileEndsWith: []
folderEndsWith: []
include: # include files and folders from indexing, if exclude is not set
files: []
folders: []
fileEndsWith: []
rootFolders: []
rootFiles: []
defaultUserScope: / # default "/" should match folders under path
defaultEnabled: true # should be added as a default source for new users?
createUserDir: false # create a user directory for each user
@@ -74,7 +76,6 @@ auth:
key: "" # the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: admin # the username of the admin user. If not set, the default is "admin".
adminPassword: admin # the password of the admin user. If not set, the default is "admin".
resetAdminOnStart: false # if set to true, the admin user will be reset to the default username and password on startup.
totpSecret: "" # secret used to encrypt TOTP secrets
frontend:
name: FileBrowser Quantum # display name
+1 -2
View File
@@ -106,9 +106,8 @@ export async function post(url, content = '', overwrite = false, onupload) {
) {
bufferContent = await new Response(content).arrayBuffer()
}
const apiPath = getApiPath('api/resources', {
path: result.path,
path: encodeURIComponent(result.path),
source: encodeURIComponent(result.source),
override: overwrite
})
+1 -1
View File
@@ -72,7 +72,7 @@ export default {
const req = state.req;
let path = "";
if (req.path !== undefined) {
path = state.req.path.replace(/#/g, "%23");
path = state.req.path.replaceAll('#', "%23");
}
let parts = path.split("/");
if (parts[0] === "") {
+37 -19
View File
@@ -230,30 +230,48 @@ export default {
return mutations.showHover(value);
},
setPositions() {
const BUFFER = 8; // px
const contextProps = getters.currentPrompt().props;
let tempX = contextProps.posX;
let tempY = contextProps.posY;
// Set initial position
this.posX = tempX;
this.posY = tempY;
// Wait for DOM update, then adjust
this.$nextTick(() => {
const menu = this.$refs.contextMenu;
if (!menu) return;
const menuWidth = menu.clientWidth || 320; // fallback to 20em
const menuHeight = menu.clientHeight || 200; // fallback to min height
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
let newX = tempX;
let newY = tempY;
// Adjust X if overflowing right
if (newX + menuWidth + BUFFER > screenWidth) {
newX = screenWidth - menuWidth - BUFFER;
}
// Adjust X if too close to left
if (newX < BUFFER) {
newX = BUFFER;
}
// Adjust Y if overflowing bottom
if (newY + menuHeight + BUFFER > screenHeight) {
newY = screenHeight - menuHeight - BUFFER;
}
// Adjust Y if too close to top
if (newY < BUFFER) {
newY = BUFFER;
}
this.posX = newX;
this.posY = newY;
});
// Show/hide create as before
if (state.selected.length > 0) {
this.showCreate = false;
} else {
this.showCreate = true;
}
const contextProps = getters.currentPrompt().props;
let tempX = contextProps.posX;
let tempY = contextProps.posY;
// Assuming the screen width and height (adjust values based on your context)
const screenWidth = window.innerWidth; // or any fixed width depending on your app's layout
const screenHeight = window.innerHeight; // or any fixed height depending on your app's layout
// if x is too close to the right edge, move it to the left by 400px
if (tempX > screenWidth - 200) {
tempX -= 200;
}
// if y is too close to the bottom edge, move it up by 400px
if (tempY > screenHeight - 400) {
tempY -= 200;
}
this.posX = tempX;
this.posY = tempY;
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
+6 -6
View File
@@ -388,16 +388,16 @@ export default {
getRelative(path) {
// double encode # to fix issue with # in path
// replace all # with %23
path = path.replace(/#/g, "%23");
path = path.replaceAll('#', "%23");
if (path.startsWith("/")) {
path = path.slice(1); // remove leading slash
}
const encodedPath = encodeURIComponent(path).replace("/%20/g", "/");
let fullpath = path;
const encodedPath = encodeURIComponent(this.getContext + path).replaceAll("%2F", "/");
let fullpath = encodedPath;
if (serverHasMultipleSources) {
fullpath = baseURL+"files/" + this.selectedSource + "/" + encodedPath;
fullpath = baseURL+"files/" + this.selectedSource + encodedPath;
} else {
fullpath = baseURL+"files/" + encodedPath;
fullpath = baseURL+"files" + encodedPath;
}
return fullpath;
},
@@ -465,7 +465,7 @@ export default {
if (string == null || string == "") {
return false;
}
this.searchTypes = this.searchTypes.replace(string + " ", "");
this.searchTypes = this.searchTypes.replaceAll(string + " ", "");
if (state.isMobile) {
this.$refs.input.focus();
}
+4 -1
View File
@@ -74,6 +74,9 @@ export default {
return this.thumbnailUrl || PLACEHOLDER_URL;
},
pdfConvertable() {
if (!muPdfAvailable) {
return false; // If muPDF is not available
}
const ext = "." + this.filename.split(".").pop().toLowerCase(); // Ensure lowercase and dot
const pdfConvertCompatibleFileExtensions = {
".pdf": true,
@@ -130,7 +133,7 @@ export default {
if (this.mimetype == "text/csv") {
return false;
}
if (this.pdfConvertable && muPdfAvailable) {
if (this.pdfConvertable) {
return true;
}
if (this.getIconForType().simpleType === "image" && state.user.preview?.image) {
+4 -21
View File
@@ -77,27 +77,10 @@ export default {
if (this.name === "") return;
// Build the path of the new directory.
let uri;
if (this.base) uri = this.base;
else if (getters.isFiles()) uri = state.route.path + "/";
else uri = "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
await filesApi.post(uri, "", true);
if (this.redirect) {
this.$router.push({ path: uri });
} else if (!this.base) {
const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/");
mutations.replaceRequest(res);
}
mutations.closeHovers();
let uri = decodeURIComponent(state.req.url);
uri += this.name + "/"; // Ensure the path ends with a slash
await filesApi.post(uri);
this.$router.push({ path: state.route.path + encodeURIComponent(this.name) });
} catch (error) {
notify.showError(error);
}
+4 -13
View File
@@ -67,19 +67,10 @@ export default {
event.preventDefault();
if (this.name === "") return;
// Build the path of the new file.
let uri = getters.isFiles() ? state.route.path + "/" : "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
await filesApi.post(uri);
this.$router.push({ path: uri });
mutations.closeHovers();
let uri = decodeURIComponent(state.req.url);
uri += this.name
await filesApi.post(uri, "", true);
this.$router.push({ path: state.route.path + encodeURIComponent(this.name) });
} catch (error) {
notify.showError(error);
}
+2 -1
View File
@@ -126,6 +126,7 @@ import { notify } from "@/notify";
import { state, getters, mutations } from "@/store";
import { shareApi, publicApi } from "@/api";
import Clipboard from "clipboard";
import { fromNow } from "@/utils/moment";
export default {
name: "share",
@@ -252,7 +253,7 @@ export default {
}
},
humanTime(time) {
return getters.getTime(time);
return fromNow(time, state.user.locale)
},
buildLink(share) {
return shareApi.getShareURL(share);
+23 -8
View File
@@ -57,26 +57,41 @@ export function generateRandomCode(length) {
}
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}`,
"Content-Type": "application/json", // Good practice
"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;
// --- CHANGE IS HERE ---
// Check if the request was successful and handle the JSON response
if (res.ok) {
const data = await res.json(); // Parse the JSON body
const logoutUrl = data.logoutUrl;
if (logoutUrl) {
// Clean up local state *before* navigating away
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
// Redirect the browser window
window.location.href = logoutUrl;
return; // Stop execution
}
} else {
// Handle potential errors from the API, e.g., res.status 401, 500
console.error("Logout API call failed:", res.status, res.statusText);
}
} catch (e) {
console.error(e);
console.error("An error occurred during logout:", e);
}
}
// Fallback for non-oidc/proxy users or if the above logic fails
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
router.push({ path: "/login" });
-1
View File
@@ -134,7 +134,6 @@ export async function handleFiles (files, base, overwrite = false) {
`(${c} of ${count}) Uploading ${relativePath}`,
false
)
console.log('Uploading', relativePath, item)
await filesApi
.post(item.path, item.file, item.overwrite, percentComplete => {
-14
View File
@@ -38,19 +38,5 @@ export default {
return errors[this.errorCode] ? errors[this.errorCode] : errors[500];
},
},
mounted() {
window.addEventListener("keydown", this.keyEvent);
},
methods: {
keyEvent(event) {
const { key } = event;
if (key == "Backspace") {
// go back
let currentPath = state.route.path.replace(/\/+$/, "");
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
router.push({ path: newPath });
}
},
},
};
</script>
+1 -1
View File
@@ -67,7 +67,7 @@ export default {
);
setTimeout(() => this.setupEditor(attempt + 1), 500);
} else {
const errorMsg = `[FATAL] Failed to sync state with the route for "${this.filename}" after 5 attempts. Aborting editor setup to prevent data corruption.`;
const errorMsg = `failed to sync state with the route for "${this.filename}" after 5 attempts. Aborting editor setup to prevent data corruption.`;
console.error(errorMsg);
notify.showError(errorMsg); // Using the custom notifier
}
+14 -3
View File
@@ -375,16 +375,27 @@ export default {
// Remove event listeners before destroying this page.
window.removeEventListener("keydown", this.keyEvent);
window.removeEventListener("resize", this.windowsResize);
this.$el.removeEventListener("contextmenu", this.openContext);
this.$el.addEventListener("touchmove", this.handleTouchMove);
window.removeEventListener("click", this.clickClear);
window.removeEventListener("keyup", this.clearCtrKey);
window.removeEventListener("dragover", this.preventDefault);
// If Safari, remove touchstart listener
this.$el.removeEventListener("touchmove", this.handleTouchMove);
this.$el.removeEventListener("contextmenu", this.openContext);
// If Safari, remove touch/mouse listeners
if (state.isSafari) {
this.$el.removeEventListener("touchstart", this.openContextForSafari);
this.$el.removeEventListener("mousedown", this.openContextForSafari);
this.$el.removeEventListener("touchend", this.cancelContext);
this.$el.removeEventListener("mouseup", this.cancelContext);
}
// Also clean up drag/drop listeners on the component's root element
if (state.user.permissions?.modify) {
this.$el.removeEventListener("dragenter", this.dragEnter);
this.$el.removeEventListener("dragleave", this.dragLeave);
this.$el.removeEventListener("drop", this.drop);
}
},
methods: {
cancelContext() {
+7 -2
View File
@@ -101,7 +101,7 @@ import { state, getters, mutations } from "@/store";
import { getFileExtension } from "@/utils/files";
import { convertToVTT } from "@/utils/subtitles";
import { getTypeInfo } from "@/utils/mimetype";
import { muPdfAvailable } from "@/utils/constants";
export default {
name: "preview",
components: {
@@ -134,6 +134,9 @@ export default {
return isIOS && isSafari;
},
pdfConvertable() {
if (!muPdfAvailable) {
return false;
}
const ext = "." + state.req.name.split(".").pop().toLowerCase(); // Ensure lowercase and dot
const pdfConvertCompatibleFileExtensions = {
".xps": true,
@@ -279,7 +282,8 @@ export default {
this.prev();
}
break;
case ("Escape", "Backspace"):
case "Escape":
case "Backspace":
this.close();
break;
}
@@ -293,6 +297,7 @@ export default {
if (playPromise !== undefined) {
playPromise.catch((error) => {
if (this.$refs.player) {
console.log("autoplay failed", error);
this.$refs.player.muted = true;
this.$refs.player.play();
}
+2 -1
View File
@@ -63,6 +63,7 @@ import { shareApi, usersApi } from "@/api";
import { state, mutations, getters } from "@/store";
import Clipboard from "clipboard";
import Errors from "@/views/Errors.vue";
import { fromNow } from '@/utils/moment'
export default {
name: "shares",
@@ -137,7 +138,7 @@ export default {
});
},
humanTime(time) {
return getters.getTime(time);
return fromNow(time);
},
buildLink(share) {
return shareApi.getShareURL(share);