mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 12:41:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
425bdbb233 | ||
|
|
595782d414 | ||
|
|
742b3510ce | ||
|
|
507a4ffa03 | ||
|
|
84c19ce029 | ||
|
|
058601d3a6 | ||
|
|
c253ccfce1 | ||
|
|
5458234b51 | ||
|
|
7228ef82c7 | ||
|
|
9d4fabdfeb |
No files matched your search
@@ -0,0 +1,24 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
|
||||
exempt-pr-labels: 'need-help,wip'
|
||||
operations-per-run: 100
|
||||
+1
-9
@@ -4,15 +4,7 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
# v0.2.1
|
||||
- issue #29 - Rules can now be configured and read from configuration yaml
|
||||
- issue #28 - Allow disable settings per user.
|
||||
- issue #27 - shorten download link for password protected files
|
||||
- issue #26 - enable dark mode per user and improve switching performance.
|
||||
- More rounded corners and improved listing styling
|
||||
- improve search performance
|
||||
- fixes authentication issues
|
||||
- adds compact view mode
|
||||
- improves view mode configuration and behavior
|
||||
- updates configuration file to accept new settings
|
||||
- More rounded corners
|
||||
|
||||
# v0.2.0
|
||||
- improved UI
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</p>
|
||||
<h3 align="center">Filebrowser - A modern file manager for the web</h3>
|
||||
<p align="center">
|
||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/b45683b0-bd55-4430-9831-650fe0d21eb8" title="Main Screenshot">
|
||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/459937ef-3f14-408d-aef5-899cde4cf3a1" title="Main Screenshot">
|
||||
</p>
|
||||
|
||||
> **NOTE**
|
||||
@@ -39,12 +39,9 @@ work better in terms of asthetics and performance. Improved search,
|
||||
## Look
|
||||
<p align="center">
|
||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/35cdeb3b-ab79-4b04-8001-8f51f6ea06bb" title="Dark mode">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/55fa4f5c-440e-4a97-b711-96139208a163">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/c76f4100-949b-4e17-a3e6-e410fb8ec08f">
|
||||
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/0bde26f3-fa90-411e-bd0b-abaa47506d62">
|
||||
<img width="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
|
||||
|
||||
|
||||
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/8d426356-26cf-407b-b078-bf58f198e799" title="Dark mode2">
|
||||
<img width="300" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/37e8f03b-4f5a-4689-aa6c-5cd858a858e9" title="Dark mode">
|
||||
<img width="300" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/b04d3c1f-154b-45ba-927c-2112926ad3a9" title="Dark mode">
|
||||
</p>
|
||||
|
||||
## Search Performance
|
||||
|
||||
+30
-17
@@ -146,11 +146,14 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
log.Println("creds", a.Cred.Password)
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// create user with the provided credentials
|
||||
d := &users.User{
|
||||
Username: a.Cred.Username,
|
||||
Password: a.Cred.Password,
|
||||
Password: pass,
|
||||
Scope: a.Settings.UserDefaults.Scope,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
@@ -175,6 +178,16 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
}
|
||||
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||
u = a.GetUser(u)
|
||||
|
||||
// update the password when it doesn't match the current
|
||||
if p {
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Password = pass
|
||||
}
|
||||
|
||||
// update user with provided fields
|
||||
err := a.Users.Update(u)
|
||||
if err != nil {
|
||||
@@ -188,31 +201,31 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
// 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.Perm.Admin
|
||||
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
|
||||
perms := users.Permissions{
|
||||
Admin: isAdmin,
|
||||
Execute: isAdmin || d.Perm.Execute,
|
||||
Create: isAdmin || d.Perm.Create,
|
||||
Rename: isAdmin || d.Perm.Rename,
|
||||
Modify: isAdmin || d.Perm.Modify,
|
||||
Delete: isAdmin || d.Perm.Delete,
|
||||
Share: isAdmin || d.Perm.Share,
|
||||
Download: isAdmin || d.Perm.Download,
|
||||
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
|
||||
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
|
||||
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
|
||||
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
|
||||
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
|
||||
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
|
||||
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
||||
}
|
||||
user := users.User{
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
Scope: d.Scope,
|
||||
Locale: d.Locale,
|
||||
Scope: a.Fields.GetString("user.scope", d.Scope),
|
||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||
ViewMode: d.ViewMode,
|
||||
SingleClick: d.SingleClick,
|
||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||
Sorting: files.Sorting{
|
||||
Asc: d.Sorting.Asc,
|
||||
By: d.Sorting.By,
|
||||
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
||||
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
||||
},
|
||||
Commands: d.Commands,
|
||||
HideDotfiles: d.HideDotfiles,
|
||||
Commands: a.Fields.GetArray("user.commands", d.Commands),
|
||||
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
|
||||
Perm: perms,
|
||||
LockPassword: true,
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||
if r.Body == nil {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
return nil, os.ErrPermission
|
||||
|
||||
@@ -5,45 +5,33 @@
|
||||
? github.com/gtsteffaniak/filebrowser/auth [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/cmd [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
|
||||
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
|
||||
? github.com/gtsteffaniak/filebrowser/errors [no test files]
|
||||
? github.com/gtsteffaniak/filebrowser/files [no test files]
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 h: 401 <nil>
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:52 Saving new user: username
|
||||
2023/10/18 10:19:53 h: 401 <nil>
|
||||
2023/10/18 10:19:53 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
2023/09/24 12:52:05 h: 401 <nil>
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.208s
|
||||
ok github.com/gtsteffaniak/filebrowser/http 0.094s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.124s
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/index
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkFillIndex-8 10 3239196 ns/op 11289 B/op 448 allocs/op
|
||||
BenchmarkSearchAllIndexes-8 10 6645964 ns/op 3176834 B/op 59104 allocs/op
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/index 0.126s
|
||||
ok github.com/gtsteffaniak/filebrowser/img 0.144s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/rules 0.002s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/runner 0.003s
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/gtsteffaniak/filebrowser/search
|
||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
|
||||
BenchmarkSearchAllIndexes-8 10 5912685 ns/op 3003976 B/op 46139 allocs/op
|
||||
BenchmarkFillIndex-8 10 3157995 ns/op 18370 B/op 449 allocs/op
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/search 0.116s
|
||||
PASS
|
||||
ok github.com/gtsteffaniak/filebrowser/settings 0.005s
|
||||
? github.com/gtsteffaniak/filebrowser/share [no test files]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(hashCmd)
|
||||
}
|
||||
|
||||
var hashCmd = &cobra.Command{
|
||||
Use: "hash <password>",
|
||||
Short: "Hashes a password",
|
||||
Long: `Hashes a password using bcrypt algorithm.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
pwd, err := users.HashPwd(args[0])
|
||||
checkErr(err)
|
||||
fmt.Println(pwd)
|
||||
},
|
||||
}
|
||||
+50
-25
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@@ -16,16 +17,22 @@ import (
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
||||
"github.com/gtsteffaniak/filebrowser/img"
|
||||
"github.com/gtsteffaniak/filebrowser/index"
|
||||
"github.com/gtsteffaniak/filebrowser/search"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile string
|
||||
)
|
||||
|
||||
type dirFS struct {
|
||||
http.Dir
|
||||
}
|
||||
@@ -64,7 +71,7 @@ var rootCmd = &cobra.Command{
|
||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||
}
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
go index.Initialize(serverConfig.IndexingInterval)
|
||||
go search.InitializeIndex(serverConfig.IndexingInterval)
|
||||
_, err := os.Stat(serverConfig.Root)
|
||||
checkErr(err)
|
||||
var listener net.Listener
|
||||
@@ -117,38 +124,56 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getRunParams(st *storage.Storage) *settings.Server {
|
||||
server, err := st.Settings.GetServer()
|
||||
checkErr(err)
|
||||
return server
|
||||
}
|
||||
|
||||
func setupLog(logMethod string) {
|
||||
switch logMethod {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
case "":
|
||||
log.SetOutput(io.Discard)
|
||||
default:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logMethod,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func quickSetup(d pythonData) {
|
||||
settings.GlobalConfiguration.Auth.Key = generateKey()
|
||||
settings.GlobalConfiguration.Key = generateKey()
|
||||
var err error
|
||||
if settings.GlobalConfiguration.Auth.Method == "noauth" {
|
||||
err := d.store.Auth.Save(&auth.NoAuth{})
|
||||
checkErr(err)
|
||||
err = d.store.Auth.Save(&auth.NoAuth{})
|
||||
} else {
|
||||
settings.GlobalConfiguration.Auth.Method = "password"
|
||||
err := d.store.Auth.Save(&auth.JSONAuth{})
|
||||
checkErr(err)
|
||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
||||
}
|
||||
err := d.store.Settings.Save(&settings.GlobalConfiguration)
|
||||
err = d.store.Settings.Save(&settings.GlobalConfiguration)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
|
||||
checkErr(err)
|
||||
user := &users.User{}
|
||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||
user.Username = settings.GlobalConfiguration.Auth.AdminUsername
|
||||
user.Password = settings.GlobalConfiguration.Auth.AdminPassword
|
||||
user.Perm.Admin = true
|
||||
user.Scope = "./"
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.LockPassword = false
|
||||
user.Perm = users.Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
username := settings.GlobalConfiguration.AdminUsername
|
||||
password := settings.GlobalConfiguration.AdminPassword
|
||||
if username == "" || password == "" {
|
||||
log.Fatal("username and password cannot be empty during quick setup")
|
||||
}
|
||||
user := &users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
LockPassword: false,
|
||||
}
|
||||
user.Perm.Admin = true
|
||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
}
|
||||
@@ -16,9 +16,12 @@ var usersAddCmd = &cobra.Command{
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
password, err := users.HashPwd(args[1])
|
||||
checkErr(err)
|
||||
|
||||
user := &users.User{
|
||||
Username: args[0],
|
||||
Password: args[1],
|
||||
Password: password,
|
||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
||||
}
|
||||
servSettings, err := d.store.Settings.GetServer()
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
|
||||
func init() {
|
||||
usersCmd.AddCommand(usersUpdateCmd)
|
||||
|
||||
usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
|
||||
usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
|
||||
}
|
||||
|
||||
var usersUpdateCmd = &cobra.Command{
|
||||
@@ -18,6 +21,9 @@ options you want to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
flags := cmd.Flags()
|
||||
password := mustGetString(flags, "password")
|
||||
newUsername := mustGetString(flags, "username")
|
||||
|
||||
var (
|
||||
err error
|
||||
@@ -30,6 +36,23 @@ options you want to change.`,
|
||||
user, err = d.store.Users.Get("", username)
|
||||
}
|
||||
checkErr(err)
|
||||
user.Scope = user.Scope
|
||||
user.Locale = user.Locale
|
||||
user.ViewMode = user.ViewMode
|
||||
user.SingleClick = user.SingleClick
|
||||
user.Perm = user.Perm
|
||||
user.Commands = user.Commands
|
||||
user.Sorting = user.Sorting
|
||||
user.LockPassword = user.LockPassword
|
||||
|
||||
if newUsername != "" {
|
||||
user.Username = newUsername
|
||||
}
|
||||
|
||||
if password != "" {
|
||||
user.Password, err = users.HashPwd(password)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
err = d.store.Users.Update(user)
|
||||
checkErr(err)
|
||||
|
||||
@@ -3,9 +3,11 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -150,3 +152,42 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("invalid format: " + ext)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range in {
|
||||
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanUpInterfaceArray(in []interface{}) []interface{} {
|
||||
result := make([]interface{}, len(in))
|
||||
for i, v := range in {
|
||||
result[i] = cleanUpMapValue(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanUpMapValue(v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case []interface{}:
|
||||
return cleanUpInterfaceArray(v)
|
||||
case map[interface{}]interface{}:
|
||||
return cleanUpInterfaceMap(v)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
||||
// then returns empty string array, else returns the splitted word array of cmd.
|
||||
// This is to ensure the result will never be []string{""}
|
||||
func convertCmdStrToCmdArray(cmd string) []string {
|
||||
var cmdArray []string
|
||||
trimmedCmdStr := strings.TrimSpace(cmd)
|
||||
if trimmedCmdStr != "" {
|
||||
cmdArray = strings.Split(trimmedCmdStr, " ")
|
||||
}
|
||||
return cmdArray
|
||||
}
|
||||
@@ -2,19 +2,12 @@ server:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
auth:
|
||||
method: password
|
||||
method: noauth
|
||||
signup: true
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
frontend:
|
||||
theme: dark
|
||||
users:
|
||||
- name: admin
|
||||
settings:
|
||||
hideDotfiles: true
|
||||
singleClick: false
|
||||
+36
-9
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4/request"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
@@ -20,8 +19,20 @@ const (
|
||||
TokenExpirationTime = time.Hour * 2
|
||||
)
|
||||
|
||||
type userInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
}
|
||||
|
||||
type authToken struct {
|
||||
User users.User `json:"user"`
|
||||
User userInfo `json:"user"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -55,7 +66,7 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
|
||||
func withUser(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return d.settings.Auth.Key, nil
|
||||
return d.settings.Key, nil
|
||||
}
|
||||
|
||||
var tk authToken
|
||||
@@ -112,7 +123,7 @@ type signupBody struct {
|
||||
}
|
||||
|
||||
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !settings.GlobalConfiguration.Auth.Signup {
|
||||
if !d.settings.Signup {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
@@ -132,9 +143,15 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
|
||||
user := &users.User{
|
||||
Username: info.Username,
|
||||
Password: info.Password,
|
||||
}
|
||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||
|
||||
pwd, err := users.HashPwd(info.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
user.Password = pwd
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
@@ -142,7 +159,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||
}
|
||||
user.Scope = userHome
|
||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
if err == errors.ErrExist {
|
||||
return http.StatusConflict, err
|
||||
@@ -159,7 +176,17 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
|
||||
|
||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||
claims := &authToken{
|
||||
User: *user,
|
||||
User: userInfo{
|
||||
ID: user.ID,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
SingleClick: user.SingleClick,
|
||||
Perm: user.Perm,
|
||||
LockPassword: user.LockPassword,
|
||||
Commands: user.Commands,
|
||||
HideDotfiles: user.HideDotfiles,
|
||||
DateFormat: user.DateFormat,
|
||||
},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
|
||||
@@ -168,7 +195,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString(d.settings.Auth.Key)
|
||||
signed, err := token.SignedString(d.settings.Key)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -85,11 +85,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
if err := storage.Users.Save(&users.User{Username: "username", Password: "pw"}); err != nil {
|
||||
t.Fatalf("failed to save user: %v", err)
|
||||
}
|
||||
if err := storage.Settings.Save(&settings.Settings{
|
||||
Auth: settings.Auth{
|
||||
Key: []byte("key"),
|
||||
},
|
||||
}); err != nil {
|
||||
if err := storage.Settings.Save(&settings.Settings{Key: []byte("key")}); err != nil {
|
||||
t.Fatalf("failed to save settings: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,18 @@ package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/index"
|
||||
"github.com/gtsteffaniak/filebrowser/search"
|
||||
)
|
||||
|
||||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
response := []map[string]interface{}{}
|
||||
query := r.URL.Query().Get("query")
|
||||
|
||||
// Retrieve the User-Agent and X-Auth headers from the request
|
||||
sessionId := r.Header.Get("SessionId")
|
||||
userScope := r.Header.Get("UserScope")
|
||||
index := *index.GetIndex()
|
||||
combinedScope := strings.TrimPrefix(userScope+r.URL.Path, ".")
|
||||
combinedScope = strings.TrimPrefix(combinedScope, "/")
|
||||
results, fileTypes := index.Search(query, combinedScope, sessionId)
|
||||
for _, path := range results {
|
||||
indexInfo, fileTypes := search.SearchAllIndexes(query, r.URL.Path, sessionId)
|
||||
for _, path := range indexInfo {
|
||||
responseObj := map[string]interface{}{
|
||||
"path": path,
|
||||
"dir": true,
|
||||
|
||||
@@ -21,9 +21,9 @@ type settingsData struct {
|
||||
|
||||
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
data := &settingsData{
|
||||
Signup: settings.GlobalConfiguration.Auth.Signup,
|
||||
CreateUserDir: settings.GlobalConfiguration.Server.CreateUserDir,
|
||||
UserHomeBasePath: settings.GlobalConfiguration.Server.UserHomeBasePath,
|
||||
Signup: d.settings.Signup,
|
||||
CreateUserDir: d.settings.CreateUserDir,
|
||||
UserHomeBasePath: d.settings.UserHomeBasePath,
|
||||
Defaults: d.settings.UserDefaults,
|
||||
Rules: d.settings.Rules,
|
||||
Frontend: d.settings.Frontend,
|
||||
@@ -41,8 +41,9 @@ var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
d.settings.Server.CreateUserDir = req.CreateUserDir
|
||||
d.settings.Server.UserHomeBasePath = req.UserHomeBasePath
|
||||
d.settings.Signup = req.Signup
|
||||
d.settings.CreateUserDir = req.CreateUserDir
|
||||
d.settings.UserHomeBasePath = req.UserHomeBasePath
|
||||
d.settings.UserDefaults = req.Defaults
|
||||
d.settings.Rules = req.Rules
|
||||
d.settings.Frontend = req.Frontend
|
||||
|
||||
@@ -129,7 +129,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
|
||||
var token string
|
||||
if len(hash) > 0 {
|
||||
tokenBuffer := make([]byte, 24) //nolint:gomnd
|
||||
tokenBuffer := make([]byte, 96) //nolint:gomnd
|
||||
if _, err := rand.Read(tokenBuffer); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -30,17 +30,17 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||
"Name": d.settings.Frontend.Name,
|
||||
"DisableExternal": d.settings.Frontend.DisableExternal,
|
||||
"DisableUsedPercentage": d.settings.Frontend.DisableUsedPercentage,
|
||||
"darkMode": settings.GlobalConfiguration.UserDefaults.DarkMode,
|
||||
"Color": d.settings.Frontend.Color,
|
||||
"BaseURL": d.server.BaseURL,
|
||||
"Version": version.Version,
|
||||
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||
"Signup": settings.GlobalConfiguration.Auth.Signup,
|
||||
"Signup": d.settings.Signup,
|
||||
"NoAuth": d.settings.Auth.Method == "noauth",
|
||||
"AuthMethod": d.settings.Auth.Method,
|
||||
"LoginPage": auther.LoginPage(),
|
||||
"CSS": false,
|
||||
"ReCaptcha": false,
|
||||
"Theme": d.settings.Frontend.Theme,
|
||||
"EnableThumbs": d.server.EnableThumbnails,
|
||||
"ResizePreview": d.server.ResizePreview,
|
||||
"EnableExec": d.server.EnableExec,
|
||||
|
||||
@@ -124,6 +124,11 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||
return http.StatusBadRequest, errors.ErrEmptyPassword
|
||||
}
|
||||
|
||||
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
|
||||
if err != nil {
|
||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||
@@ -179,6 +184,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
if !d.user.Perm.Admin && d.user.LockPassword {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/settings"
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
Dirs []string
|
||||
Files []string
|
||||
}
|
||||
|
||||
var (
|
||||
rootPath string = "/srv"
|
||||
indexes Index
|
||||
indexMutex sync.RWMutex
|
||||
lastIndexed time.Time
|
||||
)
|
||||
|
||||
func GetIndex() *Index {
|
||||
return &indexes
|
||||
}
|
||||
|
||||
func Initialize(intervalMinutes uint32) {
|
||||
// Initialize the index
|
||||
indexes = Index{
|
||||
Dirs: []string{},
|
||||
Files: []string{},
|
||||
}
|
||||
rootPath = settings.GlobalConfiguration.Server.Root
|
||||
var numFiles, numDirs int
|
||||
log.Println("Indexing files...")
|
||||
lastIndexedStart := time.Now()
|
||||
// Call the function to index files and directories
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
go indexingScheduler(intervalMinutes)
|
||||
log.Println("Successfully indexed files.")
|
||||
log.Println("Files found :", totalNumFiles)
|
||||
log.Println("Directories found :", totalNumDirs)
|
||||
}
|
||||
|
||||
func indexingScheduler(intervalMinutes uint32) {
|
||||
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
|
||||
for {
|
||||
indexes.Dirs = slices.Compact(indexes.Dirs)
|
||||
indexes.Files = slices.Compact(indexes.Files)
|
||||
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||
var numFiles, numDirs int
|
||||
lastIndexedStart := time.Now()
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
if totalNumFiles+totalNumDirs > 0 {
|
||||
log.Println("re-indexing found changes and updated the index.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromSlice(slice []string, target string) []string {
|
||||
for i, s := range slice {
|
||||
if s == target {
|
||||
// Swap the target element with the last element
|
||||
slice[i], slice[len(slice)-1] = slice[len(slice)-1], slice[i]
|
||||
// Resize the slice to exclude the last element
|
||||
slice = slice[:len(slice)-1]
|
||||
break // Exit the loop, assuming there's only one target element
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
|
||||
// Check if the current directory has been modified since last indexing
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// directory must have been deleted, remove from index
|
||||
indexes.Dirs = removeFromSlice(indexes.Dirs, path)
|
||||
indexes.Files = removeFromSlice(indexes.Files, path)
|
||||
}
|
||||
defer dir.Close()
|
||||
dirInfo, err := dir.Stat()
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Compare the last modified time of the directory with the last indexed time
|
||||
if dirInfo.ModTime().Before(lastIndexed) {
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
// Read the directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Iterate over the files and directories
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
*numDirs++
|
||||
addToIndex(path, file.Name(), true)
|
||||
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
|
||||
if err != nil {
|
||||
log.Println("Could not index :", err)
|
||||
}
|
||||
} else {
|
||||
*numFiles++
|
||||
addToIndex(path, file.Name(), false)
|
||||
}
|
||||
}
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
|
||||
func addToIndex(path string, fileName string, isDir bool) {
|
||||
indexMutex.Lock()
|
||||
defer indexMutex.Unlock()
|
||||
path = strings.TrimPrefix(path, rootPath+"/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
adjustedPath := path + "/" + fileName
|
||||
if path == rootPath {
|
||||
adjustedPath = fileName
|
||||
}
|
||||
if isDir {
|
||||
indexes.Dirs = append(indexes.Dirs, adjustedPath)
|
||||
} else {
|
||||
indexes.Files = append(indexes.Files, adjustedPath)
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BenchmarkFillIndex(b *testing.B) {
|
||||
indexes = Index{
|
||||
Dirs: []string{},
|
||||
Files: []string{},
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
}
|
||||
}
|
||||
|
||||
func createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||
addToIndex("/", dirName, true)
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
fileName := "file-" + getRandomTerm() + getRandomExtension()
|
||||
addToIndex("/"+dirName, fileName, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomPath(levels int) string {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + getRandomTerm()
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
|
||||
func getRandomTerm() string {
|
||||
wordbank := []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func getRandomExtension() string {
|
||||
wordbank := []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func generateRandomSearchTerms(numTerms int) []string {
|
||||
// Generate random search terms
|
||||
searchTerms := make([]string, numTerms)
|
||||
for i := 0; i < numTerms; i++ {
|
||||
searchTerms[i] = getRandomTerm()
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
|
||||
// JSONBytesEqual compares the JSON in two byte slices.
|
||||
func JSONBytesEqual(a, b []byte) (bool, error) {
|
||||
var j, j2 interface{}
|
||||
if err := json.Unmarshal(a, &j); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &j2); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return reflect.DeepEqual(j2, j), nil
|
||||
}
|
||||
|
||||
func TestGetIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *map[string][]string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetIndex(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeIndex(t *testing.T) {
|
||||
type args struct {
|
||||
intervalMinutes uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Initialize(tt.args.intervalMinutes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_indexingScheduler(t *testing.T) {
|
||||
type args struct {
|
||||
intervalMinutes uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
indexingScheduler(tt.args.intervalMinutes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_indexFiles(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
numFiles *int
|
||||
numDirs *int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
want1 int
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := indexFiles(tt.args.path, tt.args.numFiles, tt.args.numDirs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("indexFiles() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("indexFiles() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("indexFiles() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addToIndex(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
fileName string
|
||||
isDir bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
addToIndex(tt.args.path, tt.args.fileName, tt.args.isDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
sessionInProgress sync.Map
|
||||
mutex sync.RWMutex
|
||||
maxSearchResults = 100
|
||||
bytesInMegabyte int64 = 1000000
|
||||
)
|
||||
|
||||
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
||||
if scope == "" {
|
||||
scope = "/"
|
||||
}
|
||||
runningHash := generateRandomHash(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
searchOptions := ParseSearch(search)
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
fileListTypes := make(map[string]map[string]bool)
|
||||
matching := []string{}
|
||||
for _, searchTerm := range searchOptions.Terms {
|
||||
if searchTerm == "" {
|
||||
continue
|
||||
}
|
||||
// Iterate over the embedded index.Index fields Dirs and Files
|
||||
for _, i := range []string{"Dirs", "Files"} {
|
||||
isDir := false
|
||||
count := 0
|
||||
var paths []string
|
||||
|
||||
switch i {
|
||||
case "Dirs":
|
||||
isDir = true
|
||||
paths = si.Dirs
|
||||
case "Files":
|
||||
paths = si.Files
|
||||
}
|
||||
for _, path := range paths {
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
if count > maxSearchResults {
|
||||
break
|
||||
}
|
||||
pathName := scopedPathNameFilter(path, scope, isDir)
|
||||
if pathName == "" {
|
||||
continue
|
||||
}
|
||||
fileTypes := map[string]bool{}
|
||||
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isDir, fileTypes)
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
fileListTypes[pathName] = fileType
|
||||
matching = append(matching, pathName)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the strings based on the number of elements after splitting by "/"
|
||||
sort.Slice(matching, func(i, j int) bool {
|
||||
parts1 := strings.Split(matching[i], "/")
|
||||
parts2 := strings.Split(matching[j], "/")
|
||||
return len(parts1) < len(parts2)
|
||||
})
|
||||
return matching, fileListTypes
|
||||
}
|
||||
|
||||
func scopedPathNameFilter(pathName string, scope string, isDir bool) string {
|
||||
scope = strings.TrimPrefix(scope, "/")
|
||||
pathName = strings.TrimPrefix(pathName, "/")
|
||||
pathName = strings.TrimSuffix(pathName, "/")
|
||||
if strings.HasPrefix(pathName, scope) {
|
||||
pathName = strings.TrimPrefix(pathName, scope)
|
||||
if isDir {
|
||||
pathName = pathName + "/"
|
||||
}
|
||||
} else {
|
||||
pathName = "" // return not matched
|
||||
}
|
||||
return pathName
|
||||
}
|
||||
|
||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool, fileTypes map[string]bool) (bool, map[string]bool) {
|
||||
conditions := options.Conditions
|
||||
path := getLastPathComponent(pathName)
|
||||
// Convert to lowercase once
|
||||
if !conditions["exact"] {
|
||||
path = strings.ToLower(path)
|
||||
searchTerm = strings.ToLower(searchTerm)
|
||||
}
|
||||
if strings.Contains(path, searchTerm) {
|
||||
// Calculate fileSize only if needed
|
||||
var fileSize int64
|
||||
matchesAllConditions := true
|
||||
extension := filepath.Ext(path)
|
||||
for _, k := range AllFiletypeOptions {
|
||||
if IsMatchingType(extension, k) {
|
||||
fileTypes[k] = true
|
||||
}
|
||||
}
|
||||
fileTypes["dir"] = isDir
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
var matchesCondition bool
|
||||
switch t {
|
||||
case "larger":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize > int64(options.LargerThan)*bytesInMegabyte
|
||||
case "smaller":
|
||||
if fileSize == 0 {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesCondition = fileSize < int64(options.SmallerThan)*bytesInMegabyte
|
||||
default:
|
||||
matchesCondition = v == fileTypes[t]
|
||||
}
|
||||
if !matchesCondition {
|
||||
matchesAllConditions = false
|
||||
}
|
||||
}
|
||||
return matchesAllConditions, fileTypes
|
||||
}
|
||||
// Clear variables and return
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
func getFileSize(filepath string) int64 {
|
||||
fileInfo, err := os.Stat(rootPath + "/" + filepath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
func getLastPathComponent(path string) string {
|
||||
// Use filepath.Base to extract the last component of the path
|
||||
return filepath.Base(path)
|
||||
}
|
||||
|
||||
func generateRandomHash(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
indexes = Index{
|
||||
Dirs: []string{},
|
||||
Files: []string{},
|
||||
}
|
||||
// Create mock data
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
for _, term := range searchTerms {
|
||||
indexes.Search(term, "/", "test")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop over test files and compare output
|
||||
func TestParseSearch(t *testing.T) {
|
||||
value := ParseSearch("my test search")
|
||||
want := &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
},
|
||||
Terms: []string{"my test search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("case:exact my|test|search")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": true,
|
||||
},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"larger": true,
|
||||
},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:audio thisfile")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"audio": true,
|
||||
},
|
||||
Terms: []string{"thisfile"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchIndexes(t *testing.T) {
|
||||
indexes = Index{
|
||||
Dirs: []string{
|
||||
"/test",
|
||||
"/test/path",
|
||||
"/new/test/path",
|
||||
},
|
||||
Files: []string{
|
||||
"/test/path/file.txt",
|
||||
"/test/audio1.wav",
|
||||
"/new/test/audio.wav",
|
||||
"/new/test/video.mp4",
|
||||
"/new/test/video.MP4",
|
||||
"/new/test/path/archive.zip",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
search string
|
||||
scope string
|
||||
expectedResult []string
|
||||
expectedTypes map[string]map[string]bool
|
||||
}{
|
||||
{
|
||||
search: "audio",
|
||||
scope: "/new/",
|
||||
expectedResult: []string{"test/audio.wav"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/audio.wav": map[string]bool{"audio": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "test",
|
||||
scope: "/",
|
||||
expectedResult: []string{"test/"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"test/": map[string]bool{"dir": true},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "archive",
|
||||
scope: "/",
|
||||
expectedResult: []string{"new/test/path/archive.zip"},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"new/test/path/archive.zip": map[string]bool{"archive": true, "dir": false},
|
||||
},
|
||||
},
|
||||
{
|
||||
search: "video",
|
||||
scope: "/",
|
||||
expectedResult: []string{
|
||||
"new/test/video.mp4",
|
||||
"new/test/video.MP4",
|
||||
},
|
||||
expectedTypes: map[string]map[string]bool{
|
||||
"new/test/video.MP4": map[string]bool{"video": true, "dir": false},
|
||||
"new/test/video.mp4": map[string]bool{"video": true, "dir": false},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.search, func(t *testing.T) {
|
||||
actualResult, actualTypes := indexes.Search(tt.search, tt.scope, "")
|
||||
assert.Equal(t, tt.expectedResult, actualResult)
|
||||
if len(tt.expectedTypes) > 0 {
|
||||
for key, value := range tt.expectedTypes {
|
||||
actualValue, exists := actualTypes[key]
|
||||
assert.True(t, exists, "Expected type key '%s' not found in actual types", key)
|
||||
assert.Equal(t, value, actualValue, "Type value mismatch for key '%s'", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isDoc(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isDoc(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isDoc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFileSize(t *testing.T) {
|
||||
type args struct {
|
||||
filepath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getFileSize(tt.args.filepath); got != tt.want {
|
||||
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isArchive(t *testing.T) {
|
||||
type args struct {
|
||||
extension string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isArchive(tt.args.extension); got != tt.want {
|
||||
t.Errorf("isArchive() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLastPathComponent(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLastPathComponent(tt.args.path); got != tt.want {
|
||||
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateRandomHash(t *testing.T) {
|
||||
type args struct {
|
||||
length int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := generateRandomHash(tt.args.length); got != tt.want {
|
||||
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
package index
|
||||
package search
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var typeRegexp = regexp.MustCompile(`type:(\S+)`)
|
||||
var AllFiletypeOptions = []string{
|
||||
"image",
|
||||
"audio",
|
||||
"archive",
|
||||
"video",
|
||||
"doc",
|
||||
"dir",
|
||||
}
|
||||
|
||||
var documentTypes = []string{
|
||||
".word",
|
||||
".pdf",
|
||||
@@ -128,35 +120,3 @@ func updateSize(given string) int {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func IsMatchingType(extension string, matchType string) bool {
|
||||
mimetype := mime.TypeByExtension(extension)
|
||||
if strings.HasPrefix(mimetype, matchType) {
|
||||
return true
|
||||
}
|
||||
switch matchType {
|
||||
case "doc":
|
||||
return isDoc(extension)
|
||||
case "archive":
|
||||
return isArchive(extension)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDoc(extension string) bool {
|
||||
for _, typefile := range documentTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isArchive(extension string) bool {
|
||||
for _, typefile := range compressedFile {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
sessionInProgress sync.Map // Track session with requests in progress
|
||||
rootPath string = "/srv"
|
||||
indexes map[string][]string
|
||||
mutex sync.RWMutex
|
||||
lastIndexed time.Time
|
||||
)
|
||||
|
||||
func InitializeIndex(intervalMinutes uint32) {
|
||||
// Initialize the indexes map
|
||||
indexes = make(map[string][]string)
|
||||
indexes["dirs"] = []string{}
|
||||
indexes["files"] = []string{}
|
||||
var numFiles, numDirs int
|
||||
log.Println("Indexing files...")
|
||||
lastIndexedStart := time.Now()
|
||||
// Call the function to index files and directories
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
go indexingScheduler(intervalMinutes)
|
||||
log.Println("Successfully indexed files.")
|
||||
log.Println("Files found :", totalNumFiles)
|
||||
log.Println("Directories found :", totalNumDirs)
|
||||
}
|
||||
|
||||
func indexingScheduler(intervalMinutes uint32) {
|
||||
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
|
||||
for {
|
||||
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||
var numFiles, numDirs int
|
||||
lastIndexedStart := time.Now()
|
||||
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lastIndexed = lastIndexedStart
|
||||
if totalNumFiles+totalNumDirs > 0 {
|
||||
log.Println("re-indexing found changes and updated the index.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
|
||||
// Check if the current directory has been modified since last indexing
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
// directory must have been deleted, remove from index
|
||||
delete(indexes, path)
|
||||
}
|
||||
defer dir.Close()
|
||||
dirInfo, err := dir.Stat()
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Compare the last modified time of the directory with the last indexed time
|
||||
if dirInfo.ModTime().Before(lastIndexed) {
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
// Read the directory contents
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return *numFiles, *numDirs, err
|
||||
}
|
||||
// Iterate over the files and directories
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
*numDirs++
|
||||
addToIndex(path, file.Name(), true)
|
||||
indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
|
||||
} else {
|
||||
*numFiles++
|
||||
addToIndex(path, file.Name(), false)
|
||||
}
|
||||
}
|
||||
return *numFiles, *numDirs, nil
|
||||
}
|
||||
|
||||
func addToIndex(path string, fileName string, isDir bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
path = strings.TrimPrefix(path, rootPath+"/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
adjustedPath := path + "/" + fileName
|
||||
if path == rootPath {
|
||||
adjustedPath = fileName
|
||||
}
|
||||
if isDir {
|
||||
indexes["dirs"] = append(indexes["dirs"], adjustedPath)
|
||||
} else {
|
||||
indexes["files"] = append(indexes["files"], adjustedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func SearchAllIndexes(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
||||
runningHash := generateRandomHash(4)
|
||||
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
|
||||
|
||||
searchOptions := ParseSearch(search)
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
fileListTypes := make(map[string]map[string]bool)
|
||||
var matching []string
|
||||
maximum := 100
|
||||
|
||||
for _, searchTerm := range searchOptions.Terms {
|
||||
if searchTerm == "" {
|
||||
continue
|
||||
}
|
||||
// Iterate over the indexes
|
||||
for _, i := range []string{"dirs", "files"} {
|
||||
isdir := i == "dirs"
|
||||
count := 0
|
||||
for _, path := range indexes[i] {
|
||||
value, found := sessionInProgress.Load(sourceSession)
|
||||
if !found || value != runningHash {
|
||||
return []string{}, map[string]map[string]bool{}
|
||||
}
|
||||
if count > maximum {
|
||||
break
|
||||
}
|
||||
pathName := scopedPathNameFilter(path, scope)
|
||||
if pathName == "" {
|
||||
continue
|
||||
}
|
||||
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isdir)
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
if isdir {
|
||||
pathName = pathName + "/"
|
||||
}
|
||||
matching = append(matching, pathName)
|
||||
fileListTypes[pathName] = fileType
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the strings based on the number of elements after splitting by "/"
|
||||
sort.Slice(matching, func(i, j int) bool {
|
||||
parts1 := strings.Split(matching[i], "/")
|
||||
parts2 := strings.Split(matching[j], "/")
|
||||
return len(parts1) < len(parts2)
|
||||
})
|
||||
return matching, fileListTypes
|
||||
}
|
||||
|
||||
func scopedPathNameFilter(pathName string, scope string) string {
|
||||
scope = strings.TrimPrefix(scope, "/")
|
||||
if strings.HasPrefix(pathName, scope) {
|
||||
pathName = strings.TrimPrefix(pathName, scope)
|
||||
} else {
|
||||
pathName = ""
|
||||
}
|
||||
return pathName
|
||||
}
|
||||
|
||||
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir bool) (bool, map[string]bool) {
|
||||
conditions := options.Conditions
|
||||
path := getLastPathComponent(pathName)
|
||||
// Convert to lowercase once
|
||||
lowerSearchTerm := searchTerm
|
||||
if !conditions["exact"] {
|
||||
path = strings.ToLower(path)
|
||||
lowerSearchTerm = strings.ToLower(searchTerm)
|
||||
}
|
||||
if strings.Contains(path, lowerSearchTerm) {
|
||||
// Reuse the fileTypes map and clear its values
|
||||
fileTypes := map[string]bool{
|
||||
"audio": false,
|
||||
"image": false,
|
||||
"video": false,
|
||||
"doc": false,
|
||||
"archive": false,
|
||||
"dir": false,
|
||||
}
|
||||
// Calculate fileSize only if needed
|
||||
var fileSize int64
|
||||
if conditions["larger"] || conditions["smaller"] {
|
||||
fileSize = getFileSize(pathName)
|
||||
}
|
||||
matchesAllConditions := true
|
||||
extension := filepath.Ext(path)
|
||||
mimetype := mime.TypeByExtension(extension)
|
||||
fileTypes["audio"] = strings.HasPrefix(mimetype, "audio")
|
||||
fileTypes["image"] = strings.HasPrefix(mimetype, "image")
|
||||
fileTypes["video"] = strings.HasPrefix(mimetype, "video")
|
||||
fileTypes["doc"] = isDoc(extension)
|
||||
fileTypes["archive"] = isArchive(extension)
|
||||
fileTypes["dir"] = isDir
|
||||
for t, v := range conditions {
|
||||
if t == "exact" {
|
||||
continue
|
||||
}
|
||||
var matchesCondition bool
|
||||
switch t {
|
||||
case "larger":
|
||||
matchesCondition = fileSize > int64(options.LargerThan)*1000000
|
||||
case "smaller":
|
||||
matchesCondition = fileSize < int64(options.SmallerThan)*1000000
|
||||
default:
|
||||
matchesCondition = v == fileTypes[t]
|
||||
}
|
||||
if !matchesCondition {
|
||||
matchesAllConditions = false
|
||||
}
|
||||
}
|
||||
return matchesAllConditions, fileTypes
|
||||
}
|
||||
// Clear variables and return
|
||||
return false, map[string]bool{}
|
||||
}
|
||||
|
||||
func isDoc(extension string) bool {
|
||||
for _, typefile := range documentTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getFileSize(filepath string) int64 {
|
||||
fileInfo, err := os.Stat(rootPath + "/" + filepath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
func isArchive(extension string) bool {
|
||||
for _, typefile := range compressedFile {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getLastPathComponent(path string) string {
|
||||
// Use filepath.Base to extract the last component of the path
|
||||
return filepath.Base(path)
|
||||
}
|
||||
|
||||
func generateRandomHash(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
rand.Seed(rand.Int63()) // Automatically seeded based on current time
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func stringExistsInArray(target string, strings []string) bool {
|
||||
for _, s := range strings {
|
||||
if s == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// loop over test files and compare output
|
||||
func TestParseSearch(t *testing.T) {
|
||||
value := ParseSearch("my test search")
|
||||
want := &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
},
|
||||
Terms: []string{"my test search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("case:exact my|test|search")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": true,
|
||||
},
|
||||
Terms: []string{"my", "test", "search"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"larger": true,
|
||||
},
|
||||
Terms: []string{"test"},
|
||||
LargerThan: 100,
|
||||
SmallerThan: 1000,
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
value = ParseSearch("type:audio thisfile")
|
||||
want = &SearchOptions{
|
||||
Conditions: map[string]bool{
|
||||
"exact": false,
|
||||
"audio": true,
|
||||
},
|
||||
Terms: []string{"thisfile"},
|
||||
}
|
||||
if !reflect.DeepEqual(value, want) {
|
||||
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||
indexes = make(map[string][]string)
|
||||
|
||||
// Create mock data
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
|
||||
// Generate 100 random search terms
|
||||
searchTerms := generateRandomSearchTerms(100)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Execute the SearchAllIndexes function
|
||||
for _, term := range searchTerms {
|
||||
SearchAllIndexes(term, "/", "test")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFillIndex(b *testing.B) {
|
||||
indexes = make(map[string][]string)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||
}
|
||||
}
|
||||
|
||||
func createMockData(numDirs, numFilesPerDir int) {
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||
addToIndex("/", dirName, true)
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
fileName := "file-" + getRandomTerm() + getRandomExtension()
|
||||
addToIndex("/"+dirName, fileName, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomPath(levels int) string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + getRandomTerm()
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
|
||||
func getRandomTerm() string {
|
||||
wordbank := []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func getRandomExtension() string {
|
||||
wordbank := []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
index := rand.Intn(len(wordbank))
|
||||
return wordbank[index]
|
||||
}
|
||||
|
||||
func generateRandomSearchTerms(numTerms int) []string {
|
||||
// Generate random search terms
|
||||
searchTerms := make([]string, numTerms)
|
||||
for i := 0; i < numTerms; i++ {
|
||||
searchTerms[i] = getRandomTerm()
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
|
||||
// JSONBytesEqual compares the JSON in two byte slices.
|
||||
func JSONBytesEqual(a, b []byte) (bool, error) {
|
||||
var j, j2 interface{}
|
||||
if err := json.Unmarshal(a, &j); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &j2); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return reflect.DeepEqual(j2, j), nil
|
||||
}
|
||||
|
||||
func passedFunc(t *testing.T) {
|
||||
t.Logf("%s passed!", t.Name())
|
||||
}
|
||||
|
||||
func formatDuration(duration time.Duration) string {
|
||||
if duration >= time.Second {
|
||||
return fmt.Sprintf("%.2f seconds", duration.Seconds())
|
||||
} else if duration >= time.Millisecond {
|
||||
return fmt.Sprintf("%.2f ms", float64(duration.Milliseconds()))
|
||||
}
|
||||
return fmt.Sprintf("%.2f ns", float64(duration.Nanoseconds()))
|
||||
}
|
||||
|
||||
func formatMemory(bytes int64) string {
|
||||
sizes := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
i := 0
|
||||
for bytes >= 1024 && i < len(sizes)-1 {
|
||||
bytes /= 1024
|
||||
i++
|
||||
}
|
||||
return fmt.Sprintf("%d %s", bytes, sizes[i])
|
||||
}
|
||||
+15
-34
@@ -18,6 +18,7 @@ func Initialize(configFile string) {
|
||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
|
||||
GlobalConfiguration.Server.Root = "/srv" // hardcoded for now. TODO allow changing
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) []byte {
|
||||
@@ -25,7 +26,7 @@ func loadConfigFile(configFile string) []byte {
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err)
|
||||
GlobalConfiguration = setDefaults()
|
||||
setDefaults()
|
||||
return []byte{}
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
@@ -45,9 +46,11 @@ func loadConfigFile(configFile string) []byte {
|
||||
|
||||
func setDefaults() Settings {
|
||||
return Settings{
|
||||
Signup: true,
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
Server: Server{
|
||||
EnableThumbnails: true,
|
||||
ResizePreview: false,
|
||||
EnableExec: false,
|
||||
IndexingInterval: 5,
|
||||
Port: 8080,
|
||||
@@ -58,45 +61,23 @@ func setDefaults() Settings {
|
||||
Root: "/srv",
|
||||
},
|
||||
Auth: Auth{
|
||||
AdminUsername: "admin",
|
||||
AdminPassword: "admin",
|
||||
Method: "password",
|
||||
Signup: false,
|
||||
Method: "password",
|
||||
Recaptcha: Recaptcha{
|
||||
Host: "",
|
||||
},
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
HideDotfiles: true,
|
||||
DarkMode: false,
|
||||
DisableSettings: false,
|
||||
Locale: "en",
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
HideDotfiles: true,
|
||||
Permissions: users.Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
Delete: false,
|
||||
Share: false,
|
||||
Download: false,
|
||||
Admin: false,
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func (d *UserDefaults) Apply(u *users.User) {
|
||||
u.DisableSettings = d.DisableSettings
|
||||
u.DarkMode = d.DarkMode
|
||||
u.Scope = d.Scope
|
||||
u.Locale = d.Locale
|
||||
u.ViewMode = d.ViewMode
|
||||
u.SingleClick = d.SingleClick
|
||||
u.Perm = d.Perm
|
||||
u.Sorting = d.Sorting
|
||||
u.Commands = d.Commands
|
||||
u.HideDotfiles = d.HideDotfiles
|
||||
u.DateFormat = d.DateFormat
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Initialize(tt.args.configFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_loadConfigFile(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := loadConfigFile(tt.args.configFile); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("loadConfigFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want Settings
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := setDefaults(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("setDefaults() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,13 @@ var (
|
||||
// MakeUserDir makes the user directory according to settings.
|
||||
func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string, error) {
|
||||
userScope = strings.TrimSpace(userScope)
|
||||
if userScope == "" && s.Server.CreateUserDir {
|
||||
if userScope == "" && s.CreateUserDir {
|
||||
username = cleanUsername(username)
|
||||
if username == "" || username == "-" || username == "." {
|
||||
log.Printf("create user: invalid user for home dir creation: [%s]", username)
|
||||
return "", errors.New("invalid user for home dir creation")
|
||||
}
|
||||
userScope = path.Join(s.Server.UserHomeBasePath, username)
|
||||
userScope = path.Join(s.UserHomeBasePath, username)
|
||||
}
|
||||
|
||||
userScope = path.Join("/", userScope)
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
)
|
||||
|
||||
func TestSettings_MakeUserDir(t *testing.T) {
|
||||
type fields struct {
|
||||
Signup bool
|
||||
CreateUserDir bool
|
||||
UserHomeBasePath string
|
||||
Commands map[string][]string
|
||||
Shell []string
|
||||
AdminUsername string
|
||||
AdminPassword string
|
||||
Rules []rules.Rule
|
||||
Server Server
|
||||
Auth Auth
|
||||
Frontend Frontend
|
||||
Users []UserDefaults
|
||||
UserDefaults UserDefaults
|
||||
}
|
||||
type args struct {
|
||||
username string
|
||||
userScope string
|
||||
serverRoot string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Settings{
|
||||
Commands: tt.fields.Commands,
|
||||
Shell: tt.fields.Shell,
|
||||
Rules: tt.fields.Rules,
|
||||
Server: tt.fields.Server,
|
||||
Auth: tt.fields.Auth,
|
||||
Frontend: tt.fields.Frontend,
|
||||
Users: tt.fields.Users,
|
||||
UserDefaults: tt.fields.UserDefaults,
|
||||
}
|
||||
got, err := s.MakeUserDir(tt.args.username, tt.args.userScope, tt.args.serverRoot)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Settings.MakeUserDir() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Settings.MakeUserDir() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cleanUsername(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := cleanUsername(tt.args.s); got != tt.want {
|
||||
t.Errorf("cleanUsername() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package settings
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
// StorageBackend is a settings storage backend.
|
||||
@@ -29,8 +30,8 @@ func (s *Storage) Get() (*Settings, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if set.Server.UserHomeBasePath == "" {
|
||||
set.Server.UserHomeBasePath = DefaultUsersHomeBasePath
|
||||
if set.UserHomeBasePath == "" {
|
||||
set.UserHomeBasePath = DefaultUsersHomeBasePath
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
@@ -45,7 +46,7 @@ var defaultEvents = []string{
|
||||
|
||||
// Save saves the settings for the current instance.
|
||||
func (s *Storage) Save(set *Settings) error {
|
||||
if len(set.Auth.Key) == 0 {
|
||||
if len(set.Key) == 0 {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func (s *Storage) Save(set *Settings) error {
|
||||
}
|
||||
|
||||
if set.UserDefaults.ViewMode == "" {
|
||||
set.UserDefaults.ViewMode = "normal"
|
||||
set.UserDefaults.ViewMode = users.MosaicViewMode
|
||||
}
|
||||
|
||||
if set.Rules == nil {
|
||||
|
||||
+42
-29
@@ -5,27 +5,43 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/users"
|
||||
)
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func (d *UserDefaults) Apply(u *users.User) {
|
||||
u.Scope = d.Scope
|
||||
u.Locale = d.Locale
|
||||
u.ViewMode = d.ViewMode
|
||||
u.SingleClick = d.SingleClick
|
||||
u.Perm = d.Perm
|
||||
u.Sorting = d.Sorting
|
||||
u.Commands = d.Commands
|
||||
u.HideDotfiles = d.HideDotfiles
|
||||
u.DateFormat = d.DateFormat
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Shell []string `json:"shell"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Server Server `json:"server"`
|
||||
Auth Auth `json:"auth"`
|
||||
Frontend Frontend `json:"frontend"`
|
||||
Users []UserDefaults `json:"users,omitempty"`
|
||||
UserDefaults UserDefaults `json:"userDefaults"`
|
||||
Key []byte `json:"key"`
|
||||
Signup bool `json:"signup"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Shell []string `json:"shell"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Server Server `json:"server"`
|
||||
Auth Auth `json:"auth"`
|
||||
Frontend Frontend `json:"frontend"`
|
||||
Users []UserDefaults `json:"users,omitempty"`
|
||||
UserDefaults UserDefaults `json:"userDefaults"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Header string `json:"header"`
|
||||
Method string `json:"method"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Shell string `json:"shell"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
Key []byte `json:"key"`
|
||||
Recaptcha Recaptcha `json:"recaptcha"`
|
||||
Header string `json:"header"`
|
||||
Method string `json:"method"`
|
||||
Command string `json:"command"`
|
||||
Signup bool `json:"signup"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
|
||||
type Recaptcha struct {
|
||||
@@ -51,8 +67,6 @@ type Server struct {
|
||||
Log string `json:"log"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
@@ -60,27 +74,26 @@ type Frontend struct {
|
||||
DisableExternal bool `json:"disableExternal"`
|
||||
DisableUsedPercentage bool `json:"disableUsedPercentage"`
|
||||
Files string `json:"files"`
|
||||
Theme string `json:"theme"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Sorting struct {
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
} `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Commands []string `json:"commands,omitemptys"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
}
|
||||
@@ -28,13 +28,12 @@ frontend:
|
||||
disableExternal: true
|
||||
disableUsedPercentage: true
|
||||
files: ""
|
||||
theme: ""
|
||||
color: ""
|
||||
userDefaults:
|
||||
scope: ""
|
||||
locale: ""
|
||||
viewMode: ""
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
singleClick: true
|
||||
sorting:
|
||||
by: ""
|
||||
|
||||
@@ -73,11 +73,11 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||
}
|
||||
|
||||
func (st usersBackend) Save(user *users.User) error {
|
||||
pass, err := users.HashPwd(user.Password)
|
||||
password, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = pass
|
||||
user.Password = password
|
||||
err = st.db.Save(user)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return errors.ErrExist
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -74,7 +73,7 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
|
||||
// Update updates a user in the database.
|
||||
func (s *Storage) Update(user *User, fields ...string) error {
|
||||
err := user.Clean("")
|
||||
err := user.Clean("", fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,7 +91,6 @@ func (s *Storage) Update(user *User, fields ...string) error {
|
||||
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
log.Println("Saving new user:", user.Username)
|
||||
if err := user.Clean(""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+64
-18
@@ -6,10 +6,16 @@ import (
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/files"
|
||||
"github.com/gtsteffaniak/filebrowser/rules"
|
||||
)
|
||||
|
||||
var (
|
||||
ListViewMode = "list"
|
||||
MosaicViewMode = "mosaic"
|
||||
)
|
||||
|
||||
type Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
@@ -23,23 +29,21 @@ type Permissions struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
}
|
||||
|
||||
// GetRules implements rules.Provider.
|
||||
@@ -47,11 +51,53 @@ func (u *User) GetRules() []rules.Rule {
|
||||
return u.Rules
|
||||
}
|
||||
|
||||
var checkableFields = []string{
|
||||
"Username",
|
||||
"Password",
|
||||
"Scope",
|
||||
"ViewMode",
|
||||
"Commands",
|
||||
"Sorting",
|
||||
"Rules",
|
||||
}
|
||||
|
||||
// Clean cleans up a user and verifies if all its fields
|
||||
// are alright to be saved.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (u *User) Clean(baseScope string) error {
|
||||
func (u *User) Clean(baseScope string, fields ...string) error {
|
||||
if len(fields) == 0 {
|
||||
fields = checkableFields
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
switch field {
|
||||
case "Username":
|
||||
if u.Username == "" {
|
||||
return errors.ErrEmptyUsername
|
||||
}
|
||||
case "Password":
|
||||
if u.Password == "" {
|
||||
return errors.ErrEmptyPassword
|
||||
}
|
||||
case "ViewMode":
|
||||
if u.ViewMode == "" {
|
||||
u.ViewMode = ListViewMode
|
||||
}
|
||||
case "Commands":
|
||||
if u.Commands == nil {
|
||||
u.Commands = []string{}
|
||||
}
|
||||
case "Sorting":
|
||||
if u.Sorting.By == "" {
|
||||
u.Sorting.By = "name"
|
||||
}
|
||||
case "Rules":
|
||||
if u.Rules == nil {
|
||||
u.Rules = []rules.Rule{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if u.Fs == nil {
|
||||
scope := u.Scope
|
||||
|
||||
@@ -2,7 +2,7 @@ package version
|
||||
|
||||
var (
|
||||
// Version is the current File Browser version.
|
||||
Version = "(0.2.1)"
|
||||
Version = "(0.2.0)"
|
||||
// CommitSHA is the commmit sha.
|
||||
CommitSHA = "(unknown)"
|
||||
)
|
||||
+63
-77
@@ -7,11 +7,7 @@ This document covers the available configuration options, their defaults, and ho
|
||||
Here is an expanded config file which includes all possible configurations:
|
||||
|
||||
```
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
server:
|
||||
CreateUserDir: false
|
||||
UserHomeBasePath: ""
|
||||
indexingInterval: 5
|
||||
numImageProcessors: 4
|
||||
socket: ""
|
||||
@@ -41,12 +37,11 @@ frontend:
|
||||
disableExternal: false
|
||||
disableUsedPercentage: true
|
||||
files: ""
|
||||
theme: ""
|
||||
color: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
scope: ""
|
||||
locale: "en"
|
||||
locale: ""
|
||||
viewMode: ""
|
||||
singleClick: true
|
||||
sorting:
|
||||
@@ -69,37 +64,34 @@ userDefaults:
|
||||
Here are the defaults if nothing is set:
|
||||
|
||||
```
|
||||
adminUsername: admin
|
||||
adminPassword: admin
|
||||
server:
|
||||
enableThumbnails: true
|
||||
enableExec: false
|
||||
indexingInterval: 5
|
||||
port: 8080
|
||||
numImageProcessors: 4
|
||||
baseURL: ""
|
||||
database: database.db
|
||||
log: stdout
|
||||
root: /srv
|
||||
auth:
|
||||
method: password
|
||||
recaptcha:
|
||||
host: ""
|
||||
userDefaults:
|
||||
settingsAllowed: true
|
||||
darkMode: false
|
||||
scope: ""
|
||||
locale: "en"
|
||||
scope: "."
|
||||
lockPassword: false
|
||||
hideDotfiles: true
|
||||
permissions:
|
||||
create: true
|
||||
rename: true
|
||||
modify: true
|
||||
delete: true
|
||||
share: true
|
||||
download: true
|
||||
Signup: true
|
||||
AdminUsername: admin
|
||||
AdminPassword: admin
|
||||
Server:
|
||||
EnableThumbnails: true
|
||||
EnableExec: false
|
||||
IndexingInterval: 5
|
||||
Port: 8080
|
||||
NumImageProcessors: 4
|
||||
BaseURL: ""
|
||||
Database: database.db
|
||||
Log: stdout
|
||||
Root: /srv
|
||||
Auth:
|
||||
Method: password
|
||||
Recaptcha:
|
||||
Host: ""
|
||||
UserDefaults:
|
||||
Scope: "."
|
||||
LockPassword: false
|
||||
HideDotfiles: true
|
||||
Permissions:
|
||||
Create: true
|
||||
Rename: true
|
||||
Modify: true
|
||||
Delete: true
|
||||
Share: true
|
||||
Download: true
|
||||
```
|
||||
|
||||
## About each configuration
|
||||
@@ -108,11 +100,17 @@ userDefaults:
|
||||
|
||||
## About each configuration
|
||||
|
||||
- `Signup`: This boolean value indicates whether user signup is enabled.
|
||||
|
||||
- `AdminUsername`: This is the username of the admin user.
|
||||
|
||||
- `AdminPassword`: This is the password of the admin user.
|
||||
|
||||
### Server configuration settings
|
||||
|
||||
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes. Default: `5`
|
||||
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes (used in search only).
|
||||
|
||||
- `numImageProcessors`: This is the number of image processors available. Default: `4`
|
||||
- `numImageProcessors`: This is the number of image processors available.
|
||||
|
||||
- `socket`: This is the socket configuration.
|
||||
|
||||
@@ -120,27 +118,23 @@ userDefaults:
|
||||
|
||||
- `tlsCert`: This is the TLS certificate configuration.
|
||||
|
||||
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled on ui. Default: `true`
|
||||
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled.
|
||||
|
||||
- `resizePreview`: This boolean value determines whether preview resizing is enabled. Default: `false`
|
||||
- `resizePreview`: This boolean value determines whether preview resizing is enabled.
|
||||
|
||||
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
|
||||
|
||||
- `port`: This is the port number on which the server is running. Default: `8080`
|
||||
- `port`: This is the port number on which the server is running.
|
||||
|
||||
- `baseURL`: This is the base URL for the server. Default: `""`
|
||||
- `baseURL`: This is the base URL for the server.
|
||||
|
||||
- `address`: This is the server address configuration. Default: `0.0.0.0`
|
||||
- `address`: This is the server address configuration.
|
||||
|
||||
- `log`: This specifies the log destination. Default: `stdout`
|
||||
- `log`: This specifies the log destination (e.g., "stdout" for standard output).
|
||||
|
||||
- `database`: This is the database file path + filename that will be created if it does not already exist. If it exists, it will use the existing file. Default `database.db`
|
||||
- `database`: This is the database file path + filename that will be created if it does not already exist. If it exists, it will use the existing file.
|
||||
|
||||
- `root`: This is the root directory path. Default: `/srv`
|
||||
|
||||
- `CreateUserDir`: Boolean to create user directory on user creation. Default: `false`
|
||||
|
||||
- `UserHomeBasePath`: String to define user home directory base path. Default: `""`
|
||||
- `root`: This is the root directory path.
|
||||
|
||||
### Auth configuration settings
|
||||
|
||||
@@ -155,22 +149,17 @@ userDefaults:
|
||||
- `header`: This is the authentication header.
|
||||
|
||||
- `method`: This is the authentication method used (e.g., "json"). Possible values:
|
||||
- `password` - username and password
|
||||
- `hook` - hook authentication
|
||||
- `proxy` - proxy authentication
|
||||
- `oath` - oath authentication
|
||||
- `noauth` - no authentication/login required.
|
||||
- password - username and password
|
||||
- hook - hook authentication
|
||||
- proxy - proxy authentication
|
||||
- oath - oath authentication
|
||||
|
||||
- `command`: Deprecated: This is the authentication command.
|
||||
- `command`: This is the authentication command.
|
||||
|
||||
- `signup`: This boolean value indicates whether user signup is enabled.
|
||||
|
||||
- `shell`: This is the shell configuration.
|
||||
|
||||
- `Signup`: This boolean value indicates whether user signup is enabled on the login page. NOTE: Be mindful of `userDefaults` settings if enabled. Default: `false`
|
||||
|
||||
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||
|
||||
- `AdminPassword`: This is the password of the admin user. Default: `admin`
|
||||
|
||||
|
||||
### Frontend configuration settings
|
||||
|
||||
- `name`: This is the name of the frontend.
|
||||
@@ -184,26 +173,22 @@ userDefaults:
|
||||
- `theme`: This is the theme configuration.
|
||||
|
||||
- `color`: This is the color configuration.
|
||||
|
||||
|
||||
### UserDefaults configuration settings
|
||||
|
||||
- `darkMode`: Determines whether dark mode is enabled for the user (`true` or `false`)
|
||||
|
||||
- `settingsAllowed`: Determines whether settings page is enabled for the user (`true` or `false`)
|
||||
|
||||
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
|
||||
|
||||
- `locale`: String locale configuration. Default: `en`
|
||||
- `locale`: This is the locale configuration.
|
||||
|
||||
- `viewMode`: This is the view mode configuration. Possible values: `normal`, `compact`, `list`, and `gallery`. default: `normal`
|
||||
- `viewMode`: This is the view mode configuration.
|
||||
|
||||
- `singleClick`: This boolean value determines whether single-click is enabled. (`true` or `false`)
|
||||
- `singleClick`: This boolean value determines whether single-click is enabled.
|
||||
|
||||
- `sorting`:
|
||||
|
||||
- `by`: This is the sorting method used (e.g., "asc").
|
||||
|
||||
- `asc`: This boolean value determines the sorting order is ascending or descending. (`true` or `false`)
|
||||
- `asc`: This boolean value determines the sorting order.
|
||||
|
||||
- `permissions`:
|
||||
|
||||
@@ -223,8 +208,9 @@ userDefaults:
|
||||
|
||||
- `download`: This boolean value determines whether download permissions are granted.
|
||||
|
||||
- `commands`: Deprecated: This is a list of commands.
|
||||
- `commands`: This is a list of commands.
|
||||
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden.
|
||||
|
||||
- `dateFormat`: This boolean value determines whether date formatting is enabled.
|
||||
|
||||
- `dateFormat`: This boolean value determines whether date formatting is enabled. (`true` or `false`)
|
||||
@@ -1,16 +0,0 @@
|
||||
env:
|
||||
browser: true
|
||||
es2021: true
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:vue/vue3-essential
|
||||
parserOptions:
|
||||
ecmaVersion: latest
|
||||
sourceType: module
|
||||
plugins:
|
||||
- vue
|
||||
rules:
|
||||
vue/multi-word-component-names: off
|
||||
vue/no-reserved-component-names: warn
|
||||
vue/no-mutating-props: off
|
||||
vue/no-deprecated-v-bind-sync: warn
|
||||
Generated
-823
File diff suppressed because it is too large.
Load diff
@@ -39,8 +39,6 @@
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"postcss": {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M3245 6989 c-522 -39 -1042 -197 -1480 -449 -849 -488 -1459 -1308
|
||||
-1673 -2250 -177 -776 -89 -1582 250 -2301 368 -778 1052 -1418 1857 -1739
|
||||
903 -359 1927 -325 2812 92 778 368 1418 1052 1739 1857 359 903 325 1927 -92
|
||||
2812 -296 627 -806 1175 -1423 1529 -587 338 -1308 500 -1990 449z m555 -580
|
||||
c519 -51 1018 -245 1446 -565 788 -588 1229 -1526 1174 -2496 -16 -277 -58
|
||||
-500 -145 -763 -144 -440 -378 -819 -710 -1150 -452 -452 -1005 -730 -1655
|
||||
-832 -91 -14 -175 -18 -405 -18 -304 0 -369 6 -595 51 -1105 223 -1999 1092
|
||||
-2259 2197 -52 221 -73 412 -73 667 0 397 64 732 204 1080 304 752 886 1334
|
||||
1638 1638 431 174 895 238 1380 191z"/>
|
||||
<path d="M2670 5215 c0 -13 -44 -15 -335 -15 -352 0 -383 -3 -399 -45 -3 -9
|
||||
-6 -758 -6 -1663 0 -1168 -3 -1643 -11 -1632 -8 11 -9 8 -4 -15 3 -16 17 -41
|
||||
31 -55 l24 -25 1530 0 1530 0 24 25 c14 14 26 36 27 50 1 14 1 711 1 1550 l-2
|
||||
1526 -228 142 -229 142 -136 0 -137 0 0 -600 0 -600 -705 0 -705 0 0 615 0
|
||||
615 -135 0 c-113 0 -135 -2 -135 -15z m-264 -190 c57 -29 89 -71 103 -137 35
|
||||
-154 -98 -282 -258 -247 -55 12 -122 62 -148 113 -36 69 -12 186 49 243 62 58
|
||||
170 70 254 28z m2316 -1702 c17 -15 18 -49 18 -670 l0 -653 -1245 0 -1245 0 0
|
||||
654 c0 582 2 656 16 670 14 14 139 16 1226 16 1113 0 1213 -1 1230 -17z
|
||||
m-2602 -1363 c40 -40 13 -100 -43 -100 -60 0 -88 59 -47 100 11 11 31 20 45
|
||||
20 14 0 34 -9 45 -20z m2840 0 c41 -41 11 -100 -52 -100 -35 0 -58 24 -58 60
|
||||
0 54 71 79 110 40z"/>
|
||||
<path d="M2431 3091 c-7 -13 -7 -23 2 -35 11 -15 97 -16 1067 -14 l1055 3 0
|
||||
30 0 30 -1057 3 c-1023 2 -1058 1 -1067 -17z"/>
|
||||
<path d="M2436 2675 c-19 -19 -11 -41 17 -49 41 -11 2067 -7 2088 4 23 13 25
|
||||
46 3 54 -9 3 -483 6 -1054 6 -919 0 -1040 -2 -1054 -15z"/>
|
||||
<path d="M2447 2273 c-14 -4 -17 -13 -15 -36 l3 -32 1049 -3 c767 -1 1052 1
|
||||
1062 9 20 16 17 47 -5 59 -20 10 -2055 13 -2094 3z"/>
|
||||
<path d="M3822 5027 c-21 -23 -22 -30 -22 -293 0 -258 1 -271 20 -292 27 -29
|
||||
56 -35 140 -30 56 3 75 8 93 26 22 22 22 26 22 298 l0 276 -24 19 c-19 16 -40
|
||||
19 -115 19 -84 0 -95 -2 -114 -23z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xml:space="preserve"
|
||||
width="560"
|
||||
height="560"
|
||||
version="1.1"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
viewBox="0 0 560 560"
|
||||
id="svg44"
|
||||
sodipodi:docname="icon_raw.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"><metadata
|
||||
id="metadata48"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="711"
|
||||
id="namedview46"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.33714286"
|
||||
inkscape:cx="-172.33051"
|
||||
inkscape:cy="280"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg44" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
<![CDATA[
|
||||
.fil1 {fill:#FEFEFE}
|
||||
.fil6 {fill:#006498}
|
||||
.fil7 {fill:#0EA5EB}
|
||||
.fil8 {fill:#2979FF}
|
||||
.fil3 {fill:#2BBCFF}
|
||||
.fil0 {fill:#455A64}
|
||||
.fil4 {fill:#53C6FC}
|
||||
.fil5 {fill:#BDEAFF}
|
||||
.fil2 {fill:#332C2B;fill-opacity:0.149020}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g85"
|
||||
transform="translate(-70,-70)"><path
|
||||
class="fil1"
|
||||
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil2"
|
||||
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#332c2b;fill-opacity:0.14902003" /><path
|
||||
class="fil3"
|
||||
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2bbcff" /><path
|
||||
class="fil4"
|
||||
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#53c6fc" /><polygon
|
||||
class="fil5"
|
||||
points="305,212 418,212 418,310 305,310 "
|
||||
id="polygon17"
|
||||
style="fill:#bdeaff" /><path
|
||||
class="fil5"
|
||||
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
|
||||
id="path19"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#bdeaff" /><polygon
|
||||
class="fil6"
|
||||
points="250,470 449,470 449,483 250,483 "
|
||||
id="polygon21"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
|
||||
id="path23"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
|
||||
id="path25"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil6"
|
||||
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path27"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path29"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
|
||||
id="path31"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path33"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil1"
|
||||
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path35"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><polygon
|
||||
class="fil6"
|
||||
points="305,212 284,212 284,310 305,310 "
|
||||
id="polygon37"
|
||||
style="fill:#006498" /><path
|
||||
class="fil7"
|
||||
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
|
||||
id="path39"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#0ea5eb" /><path
|
||||
class="fil8"
|
||||
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
|
||||
id="path41"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2979ff" /></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -120,22 +120,17 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
[{[ if .darkMode -]}]
|
||||
<div id="loading dark-mode">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
[{[ else ]}]
|
||||
<div id="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div> [{[ end ]}]
|
||||
</div>
|
||||
|
||||
[{[ if .Theme -]}]
|
||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
|
||||
[{[ end ]}]
|
||||
[{[ if .CSS -]}]
|
||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||
[{[ end ]}]
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
:root {
|
||||
--background: #141D24;
|
||||
--surfacePrimary: #20292F;
|
||||
--surfaceSecondary: #3A4147;
|
||||
--divider: rgba(255, 255, 255, 0.12);
|
||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||
--textSecondary: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
#loading {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
#login {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: none) {
|
||||
header {
|
||||
background: transparent;
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
#search #input {
|
||||
background: var(--surfaceSecondary);
|
||||
border-color: var(--surfaceSecondary);
|
||||
}
|
||||
#search #input input::placeholder {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
#search.active #input {
|
||||
background: var(--surfacePrimary);
|
||||
border-color: white;
|
||||
}
|
||||
#search.active input {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
#search #result {
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
#search .boxes h3 {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.action {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
.action:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.action .counter {
|
||||
border-color: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
nav > div {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
border-color: var(--divider);
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
.breadcrumbs span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
.breadcrumbs a:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
#listing .item {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
border-color: var(--divider) !important;
|
||||
}
|
||||
|
||||
#listing .item .modified {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
#listing h2,
|
||||
#listing.list .header span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
#listing.list .header span {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
#listing.list .item.header {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
.button--flat:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.dashboard #nav ul li {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
.dashboard #nav ul li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
#result-list {
|
||||
background-color:#292929;
|
||||
}
|
||||
|
||||
.card h3,
|
||||
.dashboard #nav,
|
||||
.dashboard p label {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
.card#share input,
|
||||
.card#share select,
|
||||
.input {
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.input:hover,
|
||||
.input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.input--red {
|
||||
background: #73302D;
|
||||
}
|
||||
|
||||
.input--green {
|
||||
background: #147A41;
|
||||
}
|
||||
|
||||
.dashboard #nav .wrapper,
|
||||
.collapsible {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
.collapsible > label * {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
table th {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
.file-list li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
.file-list li:before {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
.shell {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
.shell__result {
|
||||
border-top: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
#editor-container .bar {
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
nav {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
#file-selection {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
#file-selection span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
#dropdown {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
.share__box {
|
||||
background: var(--surfacePrimary) !important;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.share__box__element {
|
||||
border-top-color: var(--divider);
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
background: var(--background);
|
||||
}
|
||||
.sizeInputWrapper {
|
||||
background: var(--background);
|
||||
color: white
|
||||
}
|
||||
.button-group button {
|
||||
background: var(--background);
|
||||
color: white
|
||||
}
|
||||
#result-desktop #result-list {
|
||||
background: #2a3137;
|
||||
max-height: unset;
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
||||
|
||||
export default {
|
||||
name: "app",
|
||||
computed: {},
|
||||
mounted() {
|
||||
const loading = document.getElementById("loading");
|
||||
loading.classList.add("done");
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading);
|
||||
}, 200);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Always load styles.css */
|
||||
@import "./css/styles.css";
|
||||
@import "./css/dark.css";
|
||||
</style>
|
||||
@@ -15,7 +15,6 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
headers: {
|
||||
"X-Auth": store.state.jwt,
|
||||
"sessionId": store.state.sessionId,
|
||||
"userScope": store.state.user.scope,
|
||||
...headers,
|
||||
},
|
||||
...rest,
|
||||
|
||||
@@ -1,44 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
id="search"
|
||||
@click="open"
|
||||
v-bind:class="{ active, ongoing, 'dark-mode': isDarkMode }"
|
||||
>
|
||||
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
|
||||
<div id="input">
|
||||
<button
|
||||
v-if="active"
|
||||
class="action"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')"
|
||||
>
|
||||
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
<input
|
||||
class="main-input"
|
||||
type="text"
|
||||
@keyup.exact="keyup"
|
||||
@input="submit"
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
:aria-label="$t('search.search')"
|
||||
:placeholder="$t('search.search')"
|
||||
/>
|
||||
<input class="main-input" type="text" @keyup.exact="keyup" @input="submit" ref="input" :autofocus="active"
|
||||
v-model.trim="value" :aria-label="$t('search.search')" :placeholder="$t('search.search')" />
|
||||
</div>
|
||||
<div v-if="isMobile && active" id="result" :class="{ hidden: !active }" ref="result">
|
||||
<div id="result-list">
|
||||
<div class="button" style="width: 100%">
|
||||
Search Context: {{ this.searchContext }}
|
||||
Search Context: {{ getContext(this.$route.path) }}
|
||||
</div>
|
||||
<ul v-show="results.length > 0">
|
||||
<li
|
||||
v-for="(s, k) in results"
|
||||
:key="k"
|
||||
@click.stop.prevent="navigateTo(s.url)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<li v-for="(s, k) in results" :key="k" @click.stop.prevent="navigateTo(s.url)" style="cursor: pointer">
|
||||
<router-link to="#" event="">
|
||||
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
|
||||
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
|
||||
@@ -47,7 +23,7 @@
|
||||
<i v-else-if="s.archive" class="material-icons archive-icons"> archive </i>
|
||||
<i v-else class="material-icons file-icons"> insert_drive_file </i>
|
||||
<span class="text-container">
|
||||
{{ basePath(s.path,s.dir) }}<b>{{ baseName(s.path) }}</b>
|
||||
{{ basePath(s.path) }}<b>{{ baseName(s.path) }}</b>
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
@@ -61,26 +37,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isEmpty">
|
||||
<button
|
||||
class="mobile-boxes"
|
||||
v-if="value.length === 0 && !showBoxes"
|
||||
@click="resetSearchFilters()"
|
||||
>
|
||||
<button class="mobile-boxes" v-if="value.length === 0 && !showBoxes" @click="resetSearchFilters()">
|
||||
Reset filters
|
||||
</button>
|
||||
<template v-if="value.length === 0 && showBoxes">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t("search.types") }}</h3>
|
||||
<div>
|
||||
<div
|
||||
class="mobile-boxes"
|
||||
tabindex="0"
|
||||
v-for="(v, k) in boxes"
|
||||
:key="k"
|
||||
role="button"
|
||||
@click="addToTypes('type:' + k)"
|
||||
:aria-label="v.label"
|
||||
>
|
||||
<div class="mobile-boxes" tabindex="0" v-for="(v, k) in boxes" :key="k" role="button"
|
||||
@click="addToTypes('type:' + k)" :aria-label="v.label">
|
||||
<i class="material-icons">{{ v.icon }}</i>
|
||||
<p>{{ v.label }}</p>
|
||||
</div>
|
||||
@@ -91,7 +56,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!isMobile && active" id="result-desktop" ref="result">
|
||||
<div class="searchContext">Search Context: {{ this.searchContext }}</div>
|
||||
<div class="searchContext">Search Context: {{ getContext(this.$route.path) }}</div>
|
||||
<div id="result-list">
|
||||
<template>
|
||||
<p v-show="isEmpty && isRunning" id="renew">
|
||||
@@ -125,51 +90,26 @@
|
||||
</p>
|
||||
</div>
|
||||
<template>
|
||||
<ButtonGroup
|
||||
:buttons="folderSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
@disableAll="folderSelectClicked()"
|
||||
@enableAll="resetButtonGroups()"
|
||||
/>
|
||||
<ButtonGroup
|
||||
:buttons="typeSelect"
|
||||
@button-clicked="addToTypes"
|
||||
@remove-button-clicked="removeFromTypes"
|
||||
:isDisabled="isTypeSelectDisabled"
|
||||
/>
|
||||
<ButtonGroup :buttons="folderSelect" @button-clicked="addToTypes" @remove-button-clicked="removeFromTypes"
|
||||
@disableAll="folderSelectClicked()" @enableAll="resetButtonGroups()" />
|
||||
<ButtonGroup :buttons="typeSelect" @button-clicked="addToTypes" @remove-button-clicked="removeFromTypes"
|
||||
:isDisabled="isTypeSelectDisabled" />
|
||||
<div class="sizeConstraints">
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Smaller Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="smallerThan"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="number"
|
||||
/>
|
||||
<input class="sizeInput" v-model="smallerThan" type="number" min="0" placeholder="number" />
|
||||
<p>MB</p>
|
||||
</div>
|
||||
<div class="sizeInputWrapper">
|
||||
<p>Larger Than:</p>
|
||||
<input
|
||||
class="sizeInput"
|
||||
v-model="largerThan"
|
||||
type="number"
|
||||
placeholder="number"
|
||||
/>
|
||||
<input class="sizeInput" v-model="largerThan" type="number" placeholder="number" />
|
||||
<p>MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<ul v-show="results.length > 0">
|
||||
<li
|
||||
v-for="(s, k) in results"
|
||||
:key="k"
|
||||
@click.stop.prevent="navigateTo(s.url)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<li v-for="(s, k) in results" :key="k" @click.stop.prevent="navigateTo(s.url)" style="cursor: pointer">
|
||||
<router-link to="#" event="">
|
||||
<i v-if="s.dir" class="material-icons folder-icons"> folder </i>
|
||||
<i v-else-if="s.audio" class="material-icons audio-icons"> volume_up </i>
|
||||
@@ -178,7 +118,7 @@
|
||||
<i v-else-if="s.archive" class="material-icons archive-icons"> archive </i>
|
||||
<i v-else class="material-icons file-icons"> insert_drive_file </i>
|
||||
<span class="text-container">
|
||||
{{ basePath(s.path,s.dir) }}<b>{{ baseName(s.path) }}</b>
|
||||
{{ basePath(s.path) }}<b>{{ baseName(s.path) }}</b>
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
@@ -202,14 +142,13 @@
|
||||
border-right: 1px solid gray;
|
||||
}
|
||||
|
||||
#result-desktop > #result-list {
|
||||
#result-desktop>#result-list {
|
||||
max-height: 80vh;
|
||||
width: 35em;
|
||||
overflow: scroll;
|
||||
padding-bottom: 1em;
|
||||
-webkit-transition: width 0.3s ease 0s;
|
||||
transition: width 0.3s ease 0s;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
#result-desktop {
|
||||
@@ -233,15 +172,13 @@
|
||||
background-color: lightgray;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#search.active #result-desktop ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.3em 0;
|
||||
margin-right: 0.3em;
|
||||
padding: .3em 0;
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
#search #result-list.active {
|
||||
@@ -264,10 +201,9 @@
|
||||
|
||||
/* Search */
|
||||
#search {
|
||||
background-color: unset;
|
||||
z-index: 3;
|
||||
z-index:3;
|
||||
position: fixed;
|
||||
top: 0.5em;
|
||||
top: .5em;
|
||||
min-width: 35em;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
@@ -319,6 +255,7 @@
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
|
||||
#search #result {
|
||||
padding-top: 1em;
|
||||
overflow: hidden;
|
||||
@@ -339,7 +276,7 @@ body.rtl #search #result {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#search #result > div > *:first-child {
|
||||
#search #result>div>*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -349,7 +286,7 @@ body.rtl #search #result {
|
||||
}
|
||||
|
||||
/* Search Results */
|
||||
body.rtl #search #result ul > * {
|
||||
body.rtl #search #result ul>* {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -377,7 +314,7 @@ body.rtl #search #result ul > * {
|
||||
}
|
||||
|
||||
#search.active #input {
|
||||
background-color: var(--background);
|
||||
background-color: lightgray;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
border-bottom-style: none;
|
||||
@@ -474,7 +411,7 @@ body.rtl #search .boxes h3 {
|
||||
width: 5em;
|
||||
border-radius: 1em;
|
||||
padding: 1em;
|
||||
backdrop-filter: invert(0.1);
|
||||
backdrop-filter: invert(.1);
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@@ -490,7 +427,7 @@ body.rtl #search .boxes h3 {
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid #ccc
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
@@ -516,7 +453,6 @@ body.rtl #search .boxes h3 {
|
||||
import ButtonGroup from "./ButtonGroup.vue";
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import { search } from "@/api";
|
||||
import { darkMode } from "@/utils/constants";
|
||||
|
||||
var boxes = {
|
||||
folder: { label: "folders", icon: "folder" },
|
||||
@@ -535,7 +471,6 @@ export default {
|
||||
name: "search",
|
||||
data: function () {
|
||||
return {
|
||||
searchContext: "./",
|
||||
largerThan: "",
|
||||
smallerThan: "",
|
||||
noneMessage: "Start typing 3 or more characters to begin searching.",
|
||||
@@ -550,7 +485,7 @@ export default {
|
||||
{ label: "Photos", value: "type:image" },
|
||||
{ label: "Audio", value: "type:audio" },
|
||||
{ label: "Videos", value: "type:video" },
|
||||
{ label: "Documents", value: "type:doc" },
|
||||
{ label: "Documents", value: "type:docs" },
|
||||
{ label: "Archives", value: "type:archive" },
|
||||
],
|
||||
value: "",
|
||||
@@ -567,11 +502,12 @@ export default {
|
||||
const resultList = document.getElementById("result-list");
|
||||
if (!active) {
|
||||
resultList.classList.remove("active");
|
||||
return;
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
resultList.classList.add("active");
|
||||
}, 100);
|
||||
|
||||
},
|
||||
show(val, old) {
|
||||
this.active = val === "search";
|
||||
@@ -602,9 +538,6 @@ export default {
|
||||
computed: {
|
||||
...mapState(["user", "show"]),
|
||||
...mapGetters(["isListing"]),
|
||||
isDarkMode() {
|
||||
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
|
||||
},
|
||||
showBoxes() {
|
||||
return this.searchTypes == "";
|
||||
},
|
||||
@@ -635,7 +568,6 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
this.searchContext = this.getContext(this.$route.path)
|
||||
this.handleResize(); // Call this once to set the initial width
|
||||
},
|
||||
methods: {
|
||||
@@ -652,18 +584,15 @@ export default {
|
||||
let path = "./" + url.substring(url.indexOf("/") + 1);
|
||||
return path.replace(/\/+$/, "") + "/";
|
||||
},
|
||||
basePath(str,isDir) {
|
||||
let parts = str.replace(/(\/$|^\/)/, "").split("/");
|
||||
basePath(str) {
|
||||
let parts = str.split("/");
|
||||
if (parts.length <= 2) {
|
||||
if (isDir) {
|
||||
return "/"
|
||||
}
|
||||
return "";
|
||||
return "/";
|
||||
}
|
||||
parts.pop();
|
||||
parts = parts.join("/") + "/";
|
||||
if (isDir) {
|
||||
parts = "/" + parts; // fix weird rtl thing
|
||||
if (str.endsWith("/")) {
|
||||
parts = "/" + parts;
|
||||
}
|
||||
return parts;
|
||||
},
|
||||
@@ -694,6 +623,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
this.searchTypes = this.searchTypes + string + " ";
|
||||
|
||||
},
|
||||
resetSearchFilters() {
|
||||
this.searchTypes = "";
|
||||
|
||||
@@ -1,99 +1,59 @@
|
||||
<template>
|
||||
<nav :class="{ active, 'dark-mode': isDarkMode }">
|
||||
<nav :class="{ active }">
|
||||
<template v-if="isLogged">
|
||||
<button
|
||||
class="action"
|
||||
@click="toRoot"
|
||||
:aria-label="$t('sidebar.myFiles')"
|
||||
:title="$t('sidebar.myFiles')"
|
||||
>
|
||||
<button class="action" @click="toRoot" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||
</button>
|
||||
<div v-if="user.perm.create">
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
>
|
||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
>
|
||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')">
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>{{ $t("sidebar.newFile") }}</span>
|
||||
</button>
|
||||
<button id="upload-button" @click="upload($event)" class="action">
|
||||
<button id="upload-button" @click="upload($event)" class="action" >
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload file</span>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="action"
|
||||
@click="toSettings"
|
||||
:aria-label="$t('sidebar.settings')"
|
||||
:title="$t('sidebar.settings')"
|
||||
>
|
||||
<button class="action" @click="toSettings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t("sidebar.settings") }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="canLogout"
|
||||
@click="logout"
|
||||
class="action"
|
||||
id="logout"
|
||||
:aria-label="$t('sidebar.logout')"
|
||||
:title="$t('sidebar.logout')"
|
||||
>
|
||||
<button v-if="canLogout" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')"
|
||||
:title="$t('sidebar.logout')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t("sidebar.logout") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.login')"
|
||||
:title="$t('sidebar.login')"
|
||||
>
|
||||
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t("sidebar.login") }}</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="signup"
|
||||
class="action"
|
||||
to="/login"
|
||||
:aria-label="$t('sidebar.signup')"
|
||||
:title="$t('sidebar.signup')"
|
||||
>
|
||||
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')"
|
||||
:title="$t('sidebar.signup')">
|
||||
<i class="material-icons">person_add</i>
|
||||
<span>{{ $t("sidebar.signup") }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
<div
|
||||
class="credits"
|
||||
v-if="$router.currentRoute.path.includes('/files/') && !disableUsedPercentage"
|
||||
>
|
||||
<div class="credits" v-if="$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
|
||||
">
|
||||
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
|
||||
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
|
||||
<span style="text-align:center">{{ usage.usedPercentage }}%</span>
|
||||
<span>{{ usage.used }} of {{ usage.total }} used</span>
|
||||
<br />
|
||||
<br>
|
||||
<span v-if="disableExternal">File Browser</span>
|
||||
<span v-else>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/gtsteffaniak/filebrowser"
|
||||
>
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://github.com/gtsteffaniak/filebrowser">
|
||||
File Browser
|
||||
</a>
|
||||
</span>
|
||||
@@ -119,7 +79,6 @@ import {
|
||||
import { files as api } from "@/api";
|
||||
import ProgressBar from "vue-simple-progress";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { darkMode } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
@@ -128,9 +87,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
isDarkMode() {
|
||||
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
|
||||
},
|
||||
...mapGetters(["isLogged"]),
|
||||
active() {
|
||||
return this.$store.state.show === "sidebar";
|
||||
@@ -171,11 +127,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
toRoot() {
|
||||
this.$router.push({ path: "/files/" }, () => {});
|
||||
this.$router.push({ path: "/files/" }, () => { });
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
toSettings() {
|
||||
this.$router.push({ path: "/settings" }, () => {});
|
||||
this.$router.push({ path: "/settings" }, () => { });
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
help() {
|
||||
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
document.removeEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
<script>
|
||||
import { logoURL } from "@/utils/constants";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
|
||||
export default {
|
||||
name: "header-bar",
|
||||
props: ["showLogo", "showMenu"],
|
||||
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
logoURL,
|
||||
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
this.clip.destroy();
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="viewMode">
|
||||
<option v-for="mode in viewModes" :key="mode" :value="mode">
|
||||
{{ mode }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ViewMode",
|
||||
props: ["viewMode"],
|
||||
data() {
|
||||
return {
|
||||
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
change(event) {
|
||||
this.$emit("update:viewMode", event.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
outline: 0;
|
||||
border: 0;
|
||||
padding: .5em 1em;
|
||||
border-radius: 1em;
|
||||
border-radius: .1em;
|
||||
cursor: pointer;
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.input {
|
||||
border-radius: 1em;
|
||||
border-radius: .1em;
|
||||
padding: .5em 1em;
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.share__box {
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
border-radius: 1em;
|
||||
border-radius: 0.2em;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,6 @@ body.rtl .breadcrumbs a {
|
||||
width: 95%;
|
||||
max-width: 30em;
|
||||
z-index: 1;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
/* Define a class .dark-mode for dark mode styles */
|
||||
.dark-mode {
|
||||
--background: #141D24;
|
||||
--surfacePrimary: #20292F;
|
||||
--surfaceSecondary: #3A4147;
|
||||
--divider: rgba(255, 255, 255, 0.12);
|
||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||
--textSecondary: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.dark-mode #loading {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.dark-mode #login {
|
||||
background: var(--background);
|
||||
}
|
||||
/* Loading */
|
||||
.dark-mode #loading {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Login */
|
||||
.dark-mode #login {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
#search.dark-mode input {
|
||||
color:white
|
||||
}
|
||||
|
||||
#search.active.dark-mode #input {
|
||||
border-color: white;
|
||||
}
|
||||
/* Search input */
|
||||
.dark-mode #search #input {
|
||||
background: var(--surfaceSecondary);
|
||||
border-color: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.dark-mode #search #input input::placeholder {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* Active Search input */
|
||||
.dark-mode #search.active #input {
|
||||
background: var(--surfacePrimary);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.dark-mode #search.active input {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Search result */
|
||||
.dark-mode #search #result {
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Search boxes */
|
||||
.dark-mode #search .boxes h3 {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Action */
|
||||
.dark-mode .action {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
.dark-mode .action:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
/* Action counter */
|
||||
.dark-mode .action .counter {
|
||||
border-color: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.dark-mode nav > div {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.dark-mode .breadcrumbs {
|
||||
border-color: var(--divider);
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
.dark-mode .breadcrumbs span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
.dark-mode .breadcrumbs a:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
/* Listing items */
|
||||
.dark-mode #listing .item {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
border-color: var(--divider) !important;
|
||||
}
|
||||
|
||||
/* Listing item modified text */
|
||||
.dark-mode #listing .item .modified {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* Listing header and span */
|
||||
.dark-mode #listing h2,
|
||||
.dark-mode #listing.list .header span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
/* Message */
|
||||
.dark-mode .message {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.dark-mode .card {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Flat button hover */
|
||||
.dark-mode .button--flat:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
/* Dashboard navigation */
|
||||
.dark-mode .dashboard #nav ul li {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
.dark-mode .dashboard #nav ul li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
#search.active.dark-mode #result {
|
||||
background-color: black;
|
||||
}
|
||||
/* Result list */
|
||||
.dark-mode #result-list {
|
||||
background-color: var(--surfacePrimary);
|
||||
color:white;
|
||||
}
|
||||
|
||||
/* Card, Dashboard navigation, and label */
|
||||
.dark-mode .card h3,
|
||||
.dark-mode .dashboard #nav,
|
||||
.dark-mode .dashboard p label {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.dark-mode .card#share input,
|
||||
.dark-mode .card#share select,
|
||||
.dark-mode .input {
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Input hover and focus */
|
||||
.dark-mode .input:hover,
|
||||
.dark-mode .input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Red input */
|
||||
.dark-mode .input--red {
|
||||
background: #73302D;
|
||||
}
|
||||
|
||||
/* Green input */
|
||||
.dark-mode .input--green {
|
||||
background: #147A41;
|
||||
}
|
||||
|
||||
/* Collapsible and label */
|
||||
.dark-mode .dashboard #nav .wrapper,
|
||||
.dark-mode .collapsible {
|
||||
border-color: var(--divider);
|
||||
}
|
||||
|
||||
.dark-mode .collapsible > label * {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Table header */
|
||||
.dark-mode table th {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* File list item */
|
||||
.dark-mode .file-list li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.dark-mode .file-list li:before {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* Shell */
|
||||
.dark-mode .shell {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Shell result */
|
||||
.dark-mode .shell__result {
|
||||
border-top: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
/* Editor container */
|
||||
.dark-mode #editor-container {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.dark-mode #editor-container .bar {
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.dark-mode nav {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
/* File selection */
|
||||
.dark-mode #file-selection {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
.dark-mode #file-selection span {
|
||||
color: var(--textPrimary) !important;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.dark-mode #dropdown {
|
||||
background: var(--surfaceSecondary) !important;
|
||||
}
|
||||
|
||||
/* Share box */
|
||||
.dark-mode .share__box {
|
||||
background: var(--surfacePrimary) !important;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Share box element */
|
||||
.dark-mode .share__box__element {
|
||||
border-top-color: var(--divider);
|
||||
}
|
||||
|
||||
/* Help button */
|
||||
.dark-mode .helpButton {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Size input wrapper */
|
||||
.dark-mode .sizeInputWrapper {
|
||||
background: var(--background);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Button group button */
|
||||
.dark-mode .button-group button {
|
||||
background: var(--background);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Result desktop */
|
||||
.dark-mode #result-desktop #result-list {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
/* Result desktop background */
|
||||
.dark-mode #result-desktop {
|
||||
background-color: var(--background);
|
||||
}
|
||||
@@ -166,7 +166,6 @@ table tr>*:last-child {
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
overflow: auto;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.card.floating {
|
||||
@@ -460,7 +459,7 @@ body.rtl .card .card-title>*:first-child {
|
||||
.card .card-action.full .action {
|
||||
flex: 1;
|
||||
padding: 2em;
|
||||
border-radius: 1em;
|
||||
border-radius: 0.2em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
+26
-150
@@ -85,52 +85,41 @@ body.rtl #listing {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#listing {
|
||||
#listing.mosaic {
|
||||
padding-top: 1em;
|
||||
margin: 0 -0.5em;
|
||||
}
|
||||
|
||||
#listing.gallery .item,
|
||||
#listing.compact .item,
|
||||
#listing.normal .item,
|
||||
#listing.list .item {
|
||||
max-width: 300px;
|
||||
#listing.mosaic .item {
|
||||
width: calc(33% - 1em);
|
||||
margin: .5em;
|
||||
padding: 0.5em;
|
||||
border-radius: 1em;
|
||||
border-radius: 0.2em;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
|
||||
}
|
||||
#listing.gallery .item {
|
||||
max-width: 300px;
|
||||
}
|
||||
#listing.list .item,
|
||||
#listing.compact .item {
|
||||
max-width: 100%;
|
||||
border-radius: 0em;
|
||||
}
|
||||
|
||||
#listing .item:hover {
|
||||
#listing.mosaic .item:hover {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
|
||||
}
|
||||
|
||||
#listing .header {
|
||||
#listing.mosaic .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing .item div:first-of-type {
|
||||
#listing.mosaic .item div:first-of-type {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
#listing .item div:last-of-type {
|
||||
#listing.mosaic .item div:last-of-type {
|
||||
width: calc(100% - 5vw);
|
||||
}
|
||||
|
||||
#listing.gallery .item div:first-of-type {
|
||||
#listing.mosaic.gallery .item div:first-of-type {
|
||||
width: 100%;
|
||||
height: 12em;
|
||||
}
|
||||
|
||||
#listing.gallery .item div:last-of-type {
|
||||
#listing.mosaic.gallery .item div:last-of-type {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
padding: 1em;
|
||||
@@ -138,19 +127,19 @@ body.rtl #listing {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#listing.gallery .item[data-type=image] div:last-of-type {
|
||||
#listing.mosaic.gallery .item[data-type=image] div:last-of-type {
|
||||
color: white;
|
||||
background: linear-gradient(#0000, #0009);
|
||||
}
|
||||
|
||||
#listing.gallery .item i {
|
||||
#listing.mosaic.gallery .item i {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
font-size: 8em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#listing.gallery .item img {
|
||||
#listing.mosaic.gallery .item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -160,109 +149,6 @@ body.rtl #listing {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing.compact {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#listing.compact .item {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
#listing.compact h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing.compact .item div:first-of-type {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
#listing.compact .item div:first-of-type i {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#listing.compact .item div:first-of-type img {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
#listing.compact .item div:last-of-type {
|
||||
width: calc(100% - 3em);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#listing.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listing.compact .item .size {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#listing.compact .header i {
|
||||
font-size: 1.5em;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
#listing.compact .header {
|
||||
display: flex !important;
|
||||
background: var(--surfacePrimary);
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#listing.compact .header>div:first-child {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#listing.compact .header .name {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
#listing.compact .header a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#listing.compact .header>div:first-child {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#listing.compact .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listing.compact .header .name {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
#listing.compact .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#listing.compact .header i {
|
||||
opacity: 0;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
#listing.compact .header p:hover i,
|
||||
#listing.compact .header .active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#listing.compact .header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#listing.list {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -274,10 +160,14 @@ body.rtl #listing {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: .5em;
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
#listing.list h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing .item[aria-selected=true] {
|
||||
background: var(--blue) !important;
|
||||
color: var(--item-selected) !important;
|
||||
@@ -310,7 +200,7 @@ body.rtl #listing {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#listing .header {
|
||||
#listing .item.header {
|
||||
display: none !important;
|
||||
background-color: #ccc;
|
||||
}
|
||||
@@ -321,34 +211,20 @@ body.rtl #listing {
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
#listing.compact .header,
|
||||
#listing.list .header {
|
||||
#listing.list .item.header {
|
||||
display: flex !important;
|
||||
background: var(--surfacePrimary);
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
background: #fafafa;
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width:100%;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#listing.list .item:first-child {
|
||||
margin-top: .5em;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
}
|
||||
|
||||
#listing.list .item:last-child {
|
||||
margin-bottom: .5em;
|
||||
border-bottom-left-radius: 1em;
|
||||
border-bottom-right-radius: 1em;
|
||||
}
|
||||
#listing.list .header>div:first-child {
|
||||
#listing.list .item.header>div:first-child {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#listing.list .header .name {
|
||||
#listing.list .item.header .name {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
@@ -356,7 +232,7 @@ body.rtl #listing {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#listing.list .header>div:first-child {
|
||||
#listing.list .item.header>div:first-child {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@@ -364,7 +240,7 @@ body.rtl #listing {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listing.list .header .name {
|
||||
#listing.list .item.header .name {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
@@ -382,7 +258,7 @@ body.rtl #listing {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#listing.list .header .active {
|
||||
#listing.list .item.header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#login {
|
||||
background: var(--background);
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
#listing.list .item div:last-of-type{
|
||||
display:block;
|
||||
width:100%;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
|
||||
#listing.list .item .size {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing.list .item .name {
|
||||
width: 60%;
|
||||
}
|
||||
@@ -36,9 +36,7 @@
|
||||
#listing {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
#listing .item {
|
||||
min-width: 100%
|
||||
}
|
||||
|
||||
body.rtl #listing {
|
||||
margin-right: unset;
|
||||
}
|
||||
@@ -119,6 +117,9 @@
|
||||
|
||||
|
||||
@media (max-width: 450px) {
|
||||
#listing.list .item .modified {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listing.list .item .name {
|
||||
width: 100%;
|
||||
|
||||
@@ -11,7 +11,9 @@ export function parseToken(token) {
|
||||
}
|
||||
|
||||
const data = JSON.parse(Base64.decode(parts[1]));
|
||||
|
||||
document.cookie = `auth=${token}; path=/`;
|
||||
|
||||
localStorage.setItem("jwt", token);
|
||||
store.commit("setJWT", token);
|
||||
store.commit("setSession", generateRandomCode(8));
|
||||
@@ -38,6 +40,7 @@ export async function login(username, password, recaptcha) {
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const body = await res.text();
|
||||
|
||||
if (res.status === 200) {
|
||||
|
||||
@@ -3,21 +3,20 @@ const disableExternal = window.FileBrowser.DisableExternal;
|
||||
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
|
||||
const baseURL = window.FileBrowser.BaseURL;
|
||||
const staticURL = window.FileBrowser.StaticURL;
|
||||
const darkMode = window.FileBrowser.darkMode;
|
||||
const recaptcha = window.FileBrowser.ReCaptcha;
|
||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
|
||||
const signup = window.FileBrowser.Signup;
|
||||
const version = window.FileBrowser.Version;
|
||||
const logoURL = `${staticURL}/img/logo.png`;
|
||||
const logoURL = `${staticURL}/img/logo.svg`;
|
||||
const noAuth = window.FileBrowser.NoAuth;
|
||||
const authMethod = window.FileBrowser.AuthMethod;
|
||||
const loginPage = window.FileBrowser.LoginPage;
|
||||
const theme = window.FileBrowser.Theme;
|
||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const origin = window.location.origin;
|
||||
|
||||
console.log(window.FileBrowser)
|
||||
export {
|
||||
name,
|
||||
disableExternal,
|
||||
@@ -31,9 +30,9 @@ export {
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme,
|
||||
enableThumbs,
|
||||
resizePreview,
|
||||
enableExec,
|
||||
origin,
|
||||
darkMode
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/header/HeaderBar";
|
||||
|
||||
const errors = {
|
||||
0: {
|
||||
@@ -31,6 +32,7 @@ const errors = {
|
||||
export default {
|
||||
name: "errors",
|
||||
components: {
|
||||
HeaderBar,
|
||||
},
|
||||
props: ["errorCode", "showHeader"],
|
||||
computed: {
|
||||
|
||||
@@ -80,10 +80,10 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
unmounted() {
|
||||
destroyed() {
|
||||
if (this.$store.state.showShell) {
|
||||
this.$store.commit("toggleShell");
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
},
|
||||
currentView(newView) {
|
||||
// Commit the new value to the store
|
||||
this.setCurrentValue(newView);
|
||||
this.setCurrentValue(this.newValue);
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["setLoading","setCurrentView"]),
|
||||
|
||||
@@ -3,35 +3,35 @@
|
||||
<div v-if="progress" class="progress">
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<listingBar :class="{ 'dark-mode-header': isDarkMode }" v-if="currentView === 'listing'"></listingBar>
|
||||
<editorBar :class="{ 'dark-mode-header': isDarkMode }" v-else-if="currentView === 'editor'"></editorBar>
|
||||
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
|
||||
<listingBar v-if="currentView === 'listing'"></listingBar>
|
||||
<editorBar v-else-if="currentView === 'editor'"></editorBar>
|
||||
<defaultBar v-else></defaultBar>
|
||||
<sidebar></sidebar>
|
||||
<main :class="{ 'dark-mode': isDarkMode }">
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
<prompts></prompts>
|
||||
<upload-files></upload-files>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorBar from "./bars/EditorBar.vue";
|
||||
import defaultBar from "./bars/Default.vue";
|
||||
import listingBar from "./bars/ListingBar.vue";
|
||||
import editorBar from "./bars/EditorBar.vue"
|
||||
import defaultBar from "./bars/Default.vue"
|
||||
import listingBar from "./bars/ListingBar.vue"
|
||||
import Prompts from "@/components/prompts/Prompts";
|
||||
import Action from "@/components/header/Action";
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import Sidebar from "@/components/Sidebar.vue";
|
||||
import UploadFiles from "../components/prompts/UploadFiles";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { darkMode } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "layout",
|
||||
components: {
|
||||
defaultBar,
|
||||
editorBar,
|
||||
listingBar,
|
||||
Action,
|
||||
Sidebar,
|
||||
Prompts,
|
||||
UploadFiles,
|
||||
@@ -47,9 +47,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(["isLogged", "progress", "isListing"]),
|
||||
...mapState(["req", "user", "state"]),
|
||||
isDarkMode() {
|
||||
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
|
||||
},
|
||||
|
||||
isExecEnabled: () => enableExec,
|
||||
currentView() {
|
||||
if (this.req.type == undefined) {
|
||||
@@ -58,7 +56,10 @@ export default {
|
||||
|
||||
if (this.req.isDir) {
|
||||
return "listing";
|
||||
} else if (this.req.type === "text" || this.req.type === "textImmutable") {
|
||||
} else if (
|
||||
this.req.type === "text" ||
|
||||
this.req.type === "textImmutable"
|
||||
) {
|
||||
return "editor";
|
||||
} else {
|
||||
return "preview";
|
||||
@@ -74,34 +75,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getTitle() {
|
||||
let title = "Title";
|
||||
if (this.$route.path.startsWith("/settings/")) {
|
||||
title = "Settings";
|
||||
let title = "Title"
|
||||
if (this.$route.path.startsWith('/settings/')) {
|
||||
title = "Settings"
|
||||
}
|
||||
return title;
|
||||
return title
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Use the class .dark-mode to apply styles conditionally */
|
||||
.dark-mode {
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.dark-mode-header {
|
||||
color:white;
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
|
||||
/* Header with backdrop-filter support */
|
||||
@supports (backdrop-filter: none) {
|
||||
.dark-mode-header {
|
||||
background: transparent;
|
||||
backdrop-filter: blur(16px) invert(0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="login" :class="{ recaptcha: recaptcha, 'dark-mode': isDarkMode }">
|
||||
<div id="login" :class="{ recaptcha: recaptcha }">
|
||||
<form @submit="submit">
|
||||
<img :src="logoURL" alt="File Browser" />
|
||||
<h1>{{ name }}</h1>
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
recaptcha,
|
||||
recaptchaKey,
|
||||
signup,
|
||||
darkMode,
|
||||
} from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
@@ -60,9 +59,6 @@ export default {
|
||||
signup: () => signup,
|
||||
name: () => name,
|
||||
logoURL: () => logoURL,
|
||||
isDarkMode() {
|
||||
return darkMode === true
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div id="nav">
|
||||
<div v-if="settingsEnabled" class="wrapper">
|
||||
<div class="wrapper">
|
||||
<ul>
|
||||
<router-link to="/settings/profile"
|
||||
><li :class="{ active: $route.path === '/settings/profile' }">
|
||||
@@ -57,10 +57,7 @@ export default {
|
||||
this.$store.commit("updateRequest", { name: "Settings" });
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user", "loading"]),
|
||||
settingsEnabled() {
|
||||
return this.user.disableSettings == false;
|
||||
},
|
||||
...mapState(["user", "loading","req"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
@@ -160,6 +160,8 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { pub as api } from "@/api";
|
||||
import moment from "moment";
|
||||
|
||||
import HeaderBar from "@/components/header/HeaderBar";
|
||||
import Action from "@/components/header/Action";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import Errors from "@/views/Errors";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
@@ -169,6 +171,8 @@ import Clipboard from "clipboard";
|
||||
export default {
|
||||
name: "share",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Breadcrumbs,
|
||||
Item,
|
||||
QrcodeVue,
|
||||
@@ -202,7 +206,7 @@ export default {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
this.clip.destroy();
|
||||
},
|
||||
|
||||
@@ -18,18 +18,24 @@
|
||||
import Vue from "vue";
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import { users, files as api } from "@/api";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import url from "@/utils/url";
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import * as upload from "@/utils/upload";
|
||||
import css from "@/utils/css";
|
||||
import throttle from "lodash.throttle";
|
||||
import Search from "@/components/Search.vue";
|
||||
|
||||
import Item from "@/components/files/ListingItem.vue";
|
||||
|
||||
export default {
|
||||
name: "listing",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Search,
|
||||
Item,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -38,7 +44,6 @@ export default {
|
||||
dragCounter: 0,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -107,9 +112,8 @@ export default {
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
compact: "view_module",
|
||||
normal: "grid_view",
|
||||
gallery: "view_list",
|
||||
mosaic: "grid_view",
|
||||
"mosaic gallery": "view_list",
|
||||
};
|
||||
return icons[this.user.viewMode];
|
||||
},
|
||||
@@ -162,7 +166,7 @@ export default {
|
||||
document.addEventListener("dragleave", this.dragLeave);
|
||||
document.addEventListener("drop", this.drop);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("scroll", this.scrollEvent);
|
||||
@@ -261,14 +265,21 @@ export default {
|
||||
},
|
||||
switchView: async function () {
|
||||
this.$store.commit("closeHovers");
|
||||
const currentIndex = this.viewModes.indexOf(this.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const modes = {
|
||||
list: "mosaic",
|
||||
mosaic: "mosaic gallery",
|
||||
"mosaic gallery": "list",
|
||||
};
|
||||
|
||||
const data = {
|
||||
id: this.user.id,
|
||||
viewMode: this.viewModes[nextIndex],
|
||||
viewMode: modes[this.user.viewMode] || "list",
|
||||
};
|
||||
users.update(data, ["viewMode"]).catch(this.$showError);
|
||||
//users.update(data, ["viewMode"]).catch(this.$showError);
|
||||
this.$store.commit("updateUser", data);
|
||||
|
||||
//this.setItemWeight();
|
||||
//this.fillWindow();
|
||||
},
|
||||
preventDefault(event) {
|
||||
// Wrapper around prevent default.
|
||||
@@ -370,7 +381,7 @@ export default {
|
||||
let columns = Math.floor(
|
||||
document.querySelector("main").offsetWidth / this.columnWidth
|
||||
);
|
||||
let items = css(["#listing .item", "#listing .item"]);
|
||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
},
|
||||
@@ -578,6 +589,7 @@ export default {
|
||||
}
|
||||
this.$store.commit("updateRequest", {});
|
||||
let uri = url.removeLastDir(this.$route.path) + "/";
|
||||
console.log(url)
|
||||
this.$router.push({ path: uri });
|
||||
},
|
||||
upload: function () {
|
||||
|
||||
@@ -30,12 +30,14 @@ import url from "@/utils/url";
|
||||
|
||||
import HeaderBar from "@/components/header/HeaderBar";
|
||||
import Action from "@/components/header/Action";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
|
||||
export default {
|
||||
name: "editor",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Breadcrumbs,
|
||||
},
|
||||
data: function () {
|
||||
return {};
|
||||
@@ -75,7 +77,7 @@ export default {
|
||||
created() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
this.editor.destroy();
|
||||
},
|
||||
|
||||
@@ -30,11 +30,13 @@ import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import { users, files as api } from "@/api";
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import url from "@/utils/url";
|
||||
import * as upload from "@/utils/upload";
|
||||
import css from "@/utils/css";
|
||||
import throttle from "lodash.throttle";
|
||||
import Search from "@/components/Search.vue";
|
||||
|
||||
import Item from "@/components/files/ListingItem.vue";
|
||||
|
||||
export default {
|
||||
name: "listing",
|
||||
@@ -42,6 +44,7 @@ export default {
|
||||
HeaderBar,
|
||||
Action,
|
||||
Search,
|
||||
Item,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -50,7 +53,6 @@ export default {
|
||||
dragCounter: 0,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -116,9 +118,8 @@ export default {
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
compact: "view_module",
|
||||
normal: "grid_view",
|
||||
gallery: "view_list",
|
||||
mosaic: "grid_view",
|
||||
"mosaic gallery": "view_list",
|
||||
};
|
||||
return icons[this.user.viewMode];
|
||||
},
|
||||
@@ -171,7 +172,7 @@ export default {
|
||||
document.addEventListener("dragleave", this.dragLeave);
|
||||
document.addEventListener("drop", this.drop);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("scroll", this.scrollEvent);
|
||||
@@ -270,14 +271,21 @@ export default {
|
||||
},
|
||||
switchView: async function () {
|
||||
this.$store.commit("closeHovers");
|
||||
const currentIndex = this.viewModes.indexOf(this.user.viewMode);
|
||||
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||
const modes = {
|
||||
list: "mosaic",
|
||||
mosaic: "mosaic gallery",
|
||||
"mosaic gallery": "list",
|
||||
};
|
||||
|
||||
const data = {
|
||||
id: this.user.id,
|
||||
viewMode: this.viewModes[nextIndex],
|
||||
viewMode: modes[this.user.viewMode] || "list",
|
||||
};
|
||||
users.update(data, ["viewMode"]).catch(this.$showError);
|
||||
//users.update(data, ["viewMode"]).catch(this.$showError);
|
||||
this.$store.commit("updateUser", data);
|
||||
|
||||
//this.setItemWeight();
|
||||
//this.fillWindow();
|
||||
},
|
||||
preventDefault(event) {
|
||||
// Wrapper around prevent default.
|
||||
@@ -379,7 +387,7 @@ export default {
|
||||
let columns = Math.floor(
|
||||
document.querySelector("main").offsetWidth / this.columnWidth
|
||||
);
|
||||
let items = css(["#listing .item", "#listing .item"]);
|
||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { files as api } from "@/api";
|
||||
import { theme } from "@/utils/constants";
|
||||
import buttons from "@/utils/buttons";
|
||||
import url from "@/utils/url";
|
||||
import ace from "ace-builds/src-min-noconflict/ace.js";
|
||||
@@ -52,7 +53,7 @@ export default {
|
||||
created() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
this.editor.destroy();
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
<div v-else id="listing" ref="listing" :class="listingViewMode + ' file-icons'">
|
||||
<div v-else id="listing" ref="listing" :class="user.viewMode + ' file-icons'">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
@@ -132,11 +132,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<div class="header-items">
|
||||
<h2>{{ $t("files.folders") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">{{ $t("files.folders") }}</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item
|
||||
v-for="item in dirs"
|
||||
@@ -153,11 +150,7 @@
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<div v-if="req.numFiles > 0">
|
||||
<div class="header-items">
|
||||
<h2>{{ $t("files.files") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="req.numFiles > 0">{{ $t("files.files") }}</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item
|
||||
v-for="item in files"
|
||||
@@ -208,19 +201,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
.header-items {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import { users, files as api } from "@/api";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import * as upload from "@/utils/upload";
|
||||
import css from "@/utils/css";
|
||||
import throttle from "lodash.throttle";
|
||||
@@ -306,15 +291,11 @@ export default {
|
||||
viewIcon() {
|
||||
const icons = {
|
||||
list: "view_module",
|
||||
compact: "view_module",
|
||||
normal: "grid_view",
|
||||
gallery: "view_list",
|
||||
mosaic: "grid_view",
|
||||
"mosaic gallery": "view_list",
|
||||
};
|
||||
return icons[this.user.viewMode];
|
||||
},
|
||||
listingViewMode() {
|
||||
return this.user.viewMode
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: this.selectedCount > 0,
|
||||
@@ -364,7 +345,7 @@ export default {
|
||||
document.addEventListener("dragleave", this.dragLeave);
|
||||
document.addEventListener("drop", this.drop);
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
// Remove event listeners before destroying this page.
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("scroll", this.scrollEvent);
|
||||
@@ -547,7 +528,7 @@ export default {
|
||||
let columns = Math.floor(
|
||||
document.querySelector("main").offsetWidth / this.columnWidth
|
||||
);
|
||||
let items = css(["#listing .item", "#listing .item"]);
|
||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
||||
if (columns === 0) columns = 1;
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@mousemove="toggleNavigation"
|
||||
@touchstart="toggleNavigation"
|
||||
>
|
||||
<div class="loading delayed" :class="{ 'dark-mode': isDarkMode }" v-if="loading">
|
||||
<div class="loading delayed" v-if="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
@@ -103,9 +103,11 @@
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { files as api } from "@/api";
|
||||
import { resizePreview, darkMode } from "@/utils/constants";
|
||||
import { resizePreview } from "@/utils/constants";
|
||||
import url from "@/utils/url";
|
||||
import throttle from "lodash.throttle";
|
||||
import HeaderBar from "@/components/header/HeaderBar";
|
||||
import Action from "@/components/header/Action";
|
||||
import ExtendedImage from "@/components/files/ExtendedImage";
|
||||
|
||||
const mediaTypes = ["image", "video", "audio", "blob"];
|
||||
@@ -113,6 +115,8 @@ const mediaTypes = ["image", "video", "audio", "blob"];
|
||||
export default {
|
||||
name: "preview",
|
||||
components: {
|
||||
HeaderBar,
|
||||
Action,
|
||||
ExtendedImage,
|
||||
},
|
||||
data: function () {
|
||||
@@ -132,9 +136,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(["req", "user", "oldReq", "jwt", "loading", "show"]),
|
||||
isDarkMode() {
|
||||
return this.user && Object.prototype.hasOwnProperty.call(this.user, "darkMode") ? this.user.darkMode : darkMode;
|
||||
},
|
||||
hasPrevious() {
|
||||
return this.previousLink !== "";
|
||||
},
|
||||
@@ -175,7 +176,7 @@ export default {
|
||||
this.listing = this.oldReq.items;
|
||||
this.updatePreview();
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keydown", this.key);
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="theme">{{ $t("settings.themes.title") }}</label>
|
||||
<themes
|
||||
class="input input--block"
|
||||
:theme.sync="settings.frontend.theme"
|
||||
id="theme"
|
||||
></themes>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
||||
<input
|
||||
@@ -185,11 +194,13 @@ import { settings as api } from "@/api";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import UserForm from "@/components/settings/UserForm";
|
||||
import Rules from "@/components/settings/Rules";
|
||||
import Themes from "@/components/settings/Themes";
|
||||
import Errors from "@/views/Errors";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
Themes,
|
||||
UserForm,
|
||||
Rules,
|
||||
Errors,
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<input type="checkbox" v-model="darkMode" />
|
||||
Dark Mode
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" v-model="hideDotfiles" />
|
||||
{{ $t("settings.hideDotfiles") }}
|
||||
@@ -23,11 +19,6 @@
|
||||
<input type="checkbox" v-model="dateFormat" />
|
||||
{{ $t("settings.setDateFormat") }}
|
||||
</p>
|
||||
<h3>Listing View Style</h3>
|
||||
<ViewMode
|
||||
class="input input--block"
|
||||
:viewMode.sync="viewMode"
|
||||
></ViewMode>
|
||||
<h3>{{ $t("settings.language") }}</h3>
|
||||
<languages
|
||||
class="input input--block"
|
||||
@@ -84,13 +75,11 @@
|
||||
import { mapState, mapMutations } from "vuex";
|
||||
import { users as api } from "@/api";
|
||||
import Languages from "@/components/settings/Languages";
|
||||
import ViewMode from "@/components/settings/ViewMode";
|
||||
import i18n, { rtlLanguages } from "@/i18n";
|
||||
|
||||
export default {
|
||||
name: "settings",
|
||||
components: {
|
||||
ViewMode,
|
||||
Languages,
|
||||
},
|
||||
data: function () {
|
||||
@@ -100,8 +89,6 @@ export default {
|
||||
hideDotfiles: false,
|
||||
singleClick: false,
|
||||
dateFormat: false,
|
||||
darkMode: false,
|
||||
viewMode: "list",
|
||||
locale: "",
|
||||
};
|
||||
},
|
||||
@@ -122,14 +109,8 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (typeof this.user.darkMode === 'undefined') {
|
||||
this.darkMode = false;
|
||||
} else {
|
||||
this.darkMode = this.user.darkMode
|
||||
}
|
||||
this.setLoading(false);
|
||||
this.locale = this.user.locale;
|
||||
this.viewMode = this.user.viewMode;
|
||||
this.hideDotfiles = this.user.hideDotfiles;
|
||||
this.singleClick = this.user.singleClick;
|
||||
this.dateFormat = this.user.dateFormat;
|
||||
@@ -154,12 +135,11 @@ export default {
|
||||
},
|
||||
async updateSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const data = {
|
||||
id: this.user.id,
|
||||
locale: this.locale,
|
||||
darkMode: this.darkMode,
|
||||
viewMode: this.viewMode,
|
||||
hideDotfiles: this.hideDotfiles,
|
||||
singleClick: this.singleClick,
|
||||
dateFormat: this.dateFormat,
|
||||
@@ -169,8 +149,6 @@ export default {
|
||||
rtlLanguages.includes(i18n.locale);
|
||||
await api.update(data, [
|
||||
"locale",
|
||||
"darkMode",
|
||||
"viewMode",
|
||||
"hideDotfiles",
|
||||
"singleClick",
|
||||
"dateFormat",
|
||||
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
this.clip.destroy();
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="showDeletePrompt" class="card floating">
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||
<div class="card-content">
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
</div>
|
||||
@@ -78,7 +78,6 @@ export default {
|
||||
error: null,
|
||||
originalUser: null,
|
||||
user: {},
|
||||
showDelete: false,
|
||||
createUserDir: false,
|
||||
};
|
||||
},
|
||||
@@ -90,9 +89,6 @@ export default {
|
||||
return this.$route.path === "/settings/users/new";
|
||||
},
|
||||
...mapState(["loading"]),
|
||||
showDeletePrompt() {
|
||||
return this.showDelete
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: "fetchData",
|
||||
@@ -129,7 +125,7 @@ export default {
|
||||
}
|
||||
},
|
||||
deletePrompt() {
|
||||
this.showDelete = true
|
||||
this.showHover("deleteUser");
|
||||
},
|
||||
async deleteUser(event) {
|
||||
event.preventDefault();
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Future releases (within 6 months):
|
||||
- Add tools to sidebar
|
||||
- duplicate file detector.
|
||||
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473
|
||||
- Add Job status to the sidebar - index status.
|
||||
- Add Job status to the sidebar - indexin status.
|
||||
- Use vite instead of webpack
|
||||
- upgrade to vue3
|
||||
- support minio/s3 https://github.com/filebrowser/filebrowser/issues/2544
|
||||
|
||||
Reference in new issue
Block a user