Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7619840a81 | ||
|
|
efbe619f0a | ||
|
|
57d3749e40 | ||
|
|
be27d9cff0 | ||
|
|
ea113ee3b6 | ||
|
|
da9414d7f2 | ||
|
|
3d1525c54b | ||
|
|
4fb44765b9 | ||
|
|
40963b26db | ||
|
|
6663a3097d | ||
|
|
0d36810a3e | ||
|
|
66f0338e8c | ||
|
|
6dd770492e | ||
|
|
e033af1305 | ||
|
|
ddd4ce2b75 | ||
|
|
9c618c2d4c | ||
|
|
4a461e5199 | ||
|
|
2db27856e0 | ||
|
|
ec2654a774 | ||
|
|
4e441168d7 |
No files matched your search
@@ -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).
|
||||
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
config:
|
||||
exclude:
|
||||
folders:
|
||||
folderPaths:
|
||||
- "excluded"
|
||||
|
||||
auth:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func GenerateYaml() {
|
||||
}
|
||||
|
||||
setupLogging()
|
||||
setupAuth()
|
||||
setupAuth(true)
|
||||
setupSources(true)
|
||||
setupUrls()
|
||||
setupFrontend()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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] === "") {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user