Compare commits

..
10 Commits
Author SHA1 Message Date
Graham Steffaniak 425bdbb233 updated workflow 2024-05-04 20:50:46 -05:00
Graham Steffaniak 595782d414 updated dockerfile 2023-10-11 09:38:14 -05:00
Graham Steffaniak 742b3510ce updated workflow 2023-10-10 10:11:47 -05:00
Graham Steffaniak 507a4ffa03 updated workflow and dockerfile 2023-10-10 10:05:37 -05:00
Graham Steffaniak 84c19ce029 updated auth 2023-10-09 09:17:23 -05:00
Graham Steffaniak 058601d3a6 Add rules to configuration (#33) 2023-09-24 14:50:05 -05:00
Graham Steffaniak c253ccfce1 Merge branch 'main' into v0.2.0 2023-09-23 13:58:19 -05:00
Graham Steffaniak 5458234b51 updated readme 2023-09-10 09:24:26 -05:00
Graham Steffaniak 7228ef82c7 Update README.md 2023-09-10 09:02:13 -05:00
Graham Steffaniak 9d4fabdfeb Create roadmap.md 2023-09-10 08:54:01 -05:00
110 changed files with 2261 additions and 4041 deletions

No files matched your search

+24
View File
@@ -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
+43 -73
View File
@@ -1,91 +1,61 @@
# Changelog
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## v0.2.3
# v0.2.1
- issue #29 - Rules can now be configured and read from configuration yaml
- More rounded corners
- Feature: token expiration time now configurable
- FIX: Hidden files are still directly accessible. (https://github.com/filebrowser/filebrowser/issues/2698)
- FIX: search/user context bug
# v0.2.0
- improved UI
- more unified coehisive look
- Adjusted header bar look and icon behavior
- The shell is dead.
- If you need to use custom commands, exec into the docker container.
- The json config file is dead.
- All configuration is done via advanced `filebrowser.yaml`
- The only flag that is allowed is flag to specify config file.
- Removed old code to migrate database versions
- Removed all unused cmd code
## v0.2.2
- CHG: **Speed:** (0m57s) - Decreased by 78% compared to the previous release.
- CHG: **Memory Usage:** (41MB) - Reduced by 45% compared to the previous release.
- Feature: Now utilizes the index for file browser listings!
- FIX: Editor issues fixed on save and themes.
## v0.2.1
- Addressed issue #29 - Rules can now be configured and read from the configuration YAML.
- Addressed issue #28 - Allows disabling settings per user.
- Addressed issue #27 - Shortened download link for password-protected files.
- Addressed issue #26 - Enables dark mode per user and improves switching performance.
- Improved styling with more rounded corners and enhanced listing design.
- Enhanced search performance.
- Fixed authentication issues.
- Added compact view mode.
- Improved view mode configuration and behavior.
- Updated the configuration file to accept new settings.
## v0.2.0
- **Improved UI:**
- Enhanced the cohesive and unified look.
- Adjusted the header bar appearance and icon behavior.
- The shell feature has been deprecated.
- Custom commands can be executed within the Docker container if needed.
- The JSON config file is no longer used.
- All configurations are now performed via the advanced `filebrowser.yaml`.
- The only allowed flag is specifying the config file.
- Removed old code for migrating database versions.
- Eliminated all unused `cmd` code.
## v0.1.4
- **Various UI fixes:**
- Reintroduced the download button to the toolbar.
- Added the upload button to the side menu.
- Adjusted breadcrumb spacing.
- Introduced a "compact" view option.
- Fixed a slash issue with CSS right-to-left (RTL) logic.
- **Various backend improvements:**
- Added session IDs to searches to prevent collisions.
- Modified search behavior to include spaces in searches.
- Prepared for full JSON configuration support.
- Made size-based searches work for both smaller and larger files.
- Modified search types not to appear in the search bar when used.
# v0.1.4
- various UI fixes
- Added download button back to toolbar
- Added upload button to side menu
- breadcrumb spacing fix
- Added "compact" view option
- fixed slash issue with css rtl logic
- various backend fixes
- search has a sessionId attached so searches don't collide
- search no longer searches by word with spaces, includes space in searches
- prepared for full json configuration
- made size search work for smaller and larger
- made search types not show up in search bar when used
## v0.1.3
- Enhanced styling with improved colors, transparency, and blur effects.
- Hid the sidebar on desktop views.
- Simplified the navbar to include three buttons:
- Open menu
- Search
- Toggle view
- Revised desktop search style and included additional search options.
- improved styling, colors, transparency, blur
- Made sidebar hidden on desktop as well
- simplified navbar to be three buttons
- open menu
- search
- toggle view
- Changed desktop search style and included additional search options.
## v0.1.2
- Updated the UI to better utilize search features:
- Added more filter options.
- Enhanced icons with colors.
- Improved GUI styling.
- Improved search performance.
- **Index Changes:**
- **Speed:** (0m32s) - Increased by 6% compared to the previous release.
- **Memory Usage:** (93MB) - Increased by 3% compared to the previous release.
- Updated UI to use search features better
- More filter options
- Better icons with colors
- GUI styling
- Improved search performance
## v0.1.1
- Improved search functionality with indexing.
- **Index Changes (Baseline Results):**
- **Speed:** (0m30s)
- **Memory Usage:** (90MB)
- Improved search with indexing
## v0.1.0
- No changes from the original.
- nothing changed from origin.
Forked from [filebrowser/filebrowser](https://github.com/filebrowser/filebrowser).
Forked from https://github.com/filebrowser/filebrowser
+34 -21
View File
@@ -6,28 +6,24 @@
</p>
<h3 align="center">Filebrowser - A modern file manager for the web</h3>
<p align="center">
<img width="800" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/899152cf-3e69-4179-aa82-752af2df3fc6" title="Main Screenshot">
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/459937ef-3f14-408d-aef5-899cde4cf3a1" title="Main Screenshot">
</p>
> [!NOTE]
> Only intended to be used with docker.
> **NOTE**
Intended for docker use only
> [!WARNING]
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
> **Warning**
Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file. `.filebrowser.json` and any flags other than `-c` and `-config` during execution WILL NO LONGER WORK. This is by design, in order to use the v0.2.0 You can mount your directory and initialize a new DB with a new default `filebrowser.yaml` which you can tweak and use in the future. Or you can copy and paste the default startup `filebrowser.yaml` below.
This fork makes the following significant changes to filebrowser for origin:
1. [x] Better search.
- Lightning fast
- realtime results as you type
1. [x] Improves search to use index instead of filesystem.
- Lightning fast, realtime results as you type
- Works with more type filters
- interactive results page.
1. [x] Revamped and simplified GUI navbar and sidebar menu.
1. [x] **IMPORTANT** Revamped configuration via `filebrowser.yml` config file.
1. [x] More configurations possible at a per-user level
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
1. [x] Additional compact view mode as well as refreshed view mode styles.
1. [x] Improved and simplified GUI navbar and sidebar menu.
1. [x] Updated version and dependencies.
1. [x] **IMPORTANT** Moved all configurations to `filebrowser.yaml`.
## About
Filebrowser provides a file managing interface within a specified directory
@@ -43,14 +39,31 @@ 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
100x faster search. However, this will be at expense of RAM. if you have < 1 million
files and folders in the given scope, the RAM usage should be less than 200MB total. RAM requirements
should scale based on the number of directories.
Also , the approx. time to fully index will vary widely based on performance. A sufficiently performant
system should fully index within the first 5 minutes, potentially within the first few seconds.
For example, a low end 11th gen i5 with SSD indexes 128K files within 1 second:
```
2023/09/09 21:38:50 Initializing with config file: filebrowser.yaml
2023/09/09 21:38:50 Indexing files...
2023/09/09 21:38:50 Listening on [::]:8080
2023/09/09 21:38:51 Successfully indexed files.
2023/09/09 21:38:51 Files found : 123452
2023/09/09 21:38:51 Directories found : 1768
2023/09/09 21:38:51 Indexing scheduler will run every 5 minutes
```
## Install
+34 -20
View File
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
@@ -43,8 +44,8 @@ func (a *HookAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
}
a.Users = usr
a.Settings = &settings.Config
a.Server = &settings.Config.Server
a.Settings = &settings.GlobalConfiguration
a.Server = &settings.GlobalConfiguration.Server
a.Cred = cred
action, err := a.RunCommand()
@@ -145,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,
@@ -174,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 {
@@ -187,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,
Sorting: users.Sorting{
Asc: d.Sorting.Asc,
By: d.Sorting.By,
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
Sorting: files.Sorting{
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,
}
+2 -1
View File
@@ -24,12 +24,13 @@ type JSONAuth struct {
// Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
config := &settings.Config
config := &settings.GlobalConfiguration
var cred jsonCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
+1 -1
View File
@@ -15,7 +15,7 @@ type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
return usr.Get(settings.Config.Server.Root, uint(1))
return usr.Get(settings.GlobalConfiguration.Server.Root, uint(1))
}
// LoginPage tells that no auth doesn't require a login page.
+1 -1
View File
@@ -21,7 +21,7 @@ type ProxyAuth struct {
// Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := usr.Get(settings.Config.Server.Root, username)
user, err := usr.Get(settings.GlobalConfiguration.Server.Root, username)
if err == errors.ErrNotExist {
return nil, os.ErrPermission
}
+17 -28
View File
@@ -7,42 +7,31 @@
PASS
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s
? github.com/gtsteffaniak/filebrowser/errors [no test files]
goos: linux
goarch: amd64
pkg: github.com/gtsteffaniak/filebrowser/files
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
BenchmarkFillIndex-8 10 3295862 ns/op 230448 B/op 1927 allocs/op
BenchmarkSearchAllIndexes-8 10 30033386 ns/op 19647893 B/op 298702 allocs/op
PASS
ok github.com/gtsteffaniak/filebrowser/files 0.392s
? github.com/gtsteffaniak/filebrowser/files [no test files]
PASS
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 Saving new user: username
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 h: 401 <nil>
2023/11/24 13:57:20 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.118s
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]
+25
View File
@@ -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)
},
}
+54 -29
View File
@@ -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"
"github.com/gtsteffaniak/filebrowser/files"
fbhttp "github.com/gtsteffaniak/filebrowser/http"
"github.com/gtsteffaniak/filebrowser/img"
"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
}
@@ -47,7 +54,7 @@ func init() {
var rootCmd = &cobra.Command{
Use: "filebrowser",
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
serverConfig := settings.Config.Server
serverConfig := settings.GlobalConfiguration.Server
if !d.hadDB {
quickSetup(d)
}
@@ -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 files.InitializeIndex(serverConfig.IndexingInterval, true)
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.Config.Auth.Key = generateKey()
if settings.Config.Auth.Method == "noauth" {
err := d.store.Auth.Save(&auth.NoAuth{})
checkErr(err)
settings.GlobalConfiguration.Key = generateKey()
var err error
if settings.GlobalConfiguration.Auth.Method == "noauth" {
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
settings.Config.Auth.Method = "password"
err := d.store.Auth.Save(&auth.JSONAuth{})
checkErr(err)
settings.GlobalConfiguration.Auth.Method = "password"
err = d.store.Auth.Save(&auth.JSONAuth{})
}
err := d.store.Settings.Save(&settings.Config)
err = d.store.Settings.Save(&settings.GlobalConfiguration)
checkErr(err)
err = d.store.Settings.SaveServer(&settings.Config.Server)
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
checkErr(err)
user := &users.User{}
settings.Config.UserDefaults.Apply(user)
user.Username = settings.Config.Auth.AdminUsername
user.Password = settings.Config.Auth.AdminPassword
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
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,
}
settings.GlobalConfiguration.UserDefaults.Apply(user)
err = d.store.Users.Save(user)
checkErr(err)
}
+4 -1
View File
@@ -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()
+23
View File
@@ -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)
+42 -1
View File
@@ -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"
@@ -83,7 +85,7 @@ func dbExists(path string) (bool, error) {
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
path := settings.Config.Server.Database
path := settings.GlobalConfiguration.Server.Database
exists, err := dbExists(path)
if err != nil {
@@ -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
}
+10 -18
View File
@@ -1,21 +1,13 @@
server:
port: 8080
baseURL: "/"
root: "/srv"
baseURL: "/"
auth:
method: password
signup: false
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
method: noauth
signup: true
frontend:
theme: dark
users:
- name: admin
settings:
hideDotfiles: true
singleClick: false
+137 -229
View File
@@ -8,40 +8,33 @@ import (
"encoding/hex"
"hash"
"io"
"log"
"mime"
"net/http"
"os"
filepath "path/filepath"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/spf13/afero"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/users"
)
var (
bytesInMegabyte int64 = 1000000
pathMutexes = make(map[string]*sync.Mutex)
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path,omitempty"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"-"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
CacheTime time.Time `json:"-"`
Mode os.FileMode `json:"-"`
IsDir bool `json:"isDir,omitempty"`
IsSymlink bool `json:"isSymlink,omitempty"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
@@ -61,22 +54,6 @@ type FileOptions struct {
Content bool
}
// Sorting constants
const (
SortingByName = "name"
SortingBySize = "size"
SortingByModified = "modified"
)
// Listing is a collection of files.
type Listing struct {
Items []*FileInfo `json:"items"`
Path string `json:"path"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting users.Sorting `json:"sorting"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
@@ -84,126 +61,37 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
file, err := stat(opts.Path, opts) // Pass opts.Path here
file, err := stat(opts)
if err != nil {
return nil, err
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Path, opts.Modify, opts.Content, true)
err = file.detectType(opts.Modify, opts.Content, true)
if err != nil {
return nil, err
}
}
return file, err
}
func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
// Lock access for the specific path
pathMutex := getMutex(opts.Path)
pathMutex.Lock()
defer pathMutex.Unlock()
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
}
adjustedPath := makeIndexPath(trimmed, index.Root)
var info FileInfo
info, exists := index.GetMetadataInfo(adjustedPath)
if exists {
// Check if the cache time is less than 1 second
if time.Since(info.CacheTime) > time.Second {
go refreshFileInfo(opts)
}
// refresh cache after
return &info, nil
} else {
updated := refreshFileInfo(opts)
if !updated {
file, err := NewFileInfo(opts)
return file, err
}
info, exists = index.GetMetadataInfo(adjustedPath)
if !exists || info.Name == "" {
return &FileInfo{}, errors.ErrEmptyKey
}
return &info, nil
}
}
func refreshFileInfo(opts FileOptions) bool {
if !opts.Checker.Check(opts.Path) {
return false
}
file, err := stat(opts.Path, opts) // Pass opts.Path here
if err != nil {
return false
}
index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/")
if trimmed == "" {
trimmed = "/"
}
adjustedPath := makeIndexPath(trimmed, index.Root)
if file.IsDir {
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
if err != nil {
return false
}
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
} else {
//_, exists := index.GetFileMetadata(adjustedPath)
return index.UpdateFileMetadata(adjustedPath, *file)
}
}
func stat(path string, opts FileOptions) (*FileInfo, error) {
func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(path)
if err == nil {
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if info.IsDir() {
file.IsDir = true
}
if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true
}
}
}
if file == nil || file.IsSymlink {
info, err := opts.Fs.Stat(opts.Path)
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
if err != nil {
return nil, err
}
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
@@ -211,12 +99,47 @@ func stat(path string, opts FileOptions) (*FileInfo, error) {
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
IsSymlink: IsSymlink(info.Mode()),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
}
// regular file
if file != nil && !file.IsSymlink {
return file, nil
}
// fs doesn't support afero.Lstater interface or the file is a symlink
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
// can't follow symlink
if file != nil && file.IsSymlink {
return file, nil
}
return nil, err
}
// set correct file size in case of symlink
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
return file, nil
}
@@ -237,15 +160,19 @@ func (i *FileInfo) Checksum(algo string) error {
}
defer reader.Close()
hashFuncs := map[string]hash.Hash{
"md5": md5.New(),
"sha1": sha1.New(),
"sha256": sha256.New(),
"sha512": sha512.New(),
}
var h hash.Hash
h, ok := hashFuncs[algo]
if !ok {
//nolint:gosec
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return errors.ErrInvalidOption
}
@@ -258,7 +185,6 @@ func (i *FileInfo) Checksum(algo string) error {
return nil
}
// RealPath gets the real path for the file, resolving symlinks if supported.
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
@@ -272,57 +198,72 @@ func (i *FileInfo) RealPath() string {
return i.Path
}
// detectType detects the file type.
func (i *FileInfo) detectType(path string, modify, saveContent, readHeader bool) error {
// TODO: use constants
//
//nolint:goconst
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
return nil
}
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
mimetype := mime.TypeByExtension(i.Extension)
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" {
http.DetectContentType(buffer)
mimetype = http.DetectContentType(buffer)
}
}
ext := filepath.Ext(i.Name)
for _, fileType := range AllFiletypeOptions {
if IsMatchingType(ext, fileType) {
i.Type = fileType
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf"
return nil
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text"
if !modify {
i.Type = "textImmutable"
}
switch i.Type {
case "text":
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(path)
if err != nil {
return err
}
i.Content = string(content)
}
case "video":
parentDir := strings.TrimRight(path, i.Name)
i.detectSubtitles(parentDir)
case "doc":
if ext == ".pdf" {
i.Type = "pdf"
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
i.Content = string(content)
}
}
if i.Type == "" {
return nil
default:
i.Type = "blob"
}
return nil
}
// readFirstBytes reads the first bytes of the file.
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
if err != nil {
log.Print(err)
i.Type = "blob"
return nil
}
@@ -331,6 +272,7 @@ func (i *FileInfo) readFirstBytes() []byte {
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
log.Print(err)
i.Type = "blob"
return nil
}
@@ -338,43 +280,29 @@ func (i *FileInfo) readFirstBytes() []byte {
return buffer[:n]
}
// detectSubtitles detects subtitles for video files.
func (i *FileInfo) detectSubtitles(parentDir string) {
func (i *FileInfo) detectSubtitles() {
if i.Type != "video" {
return
}
i.Subtitles = []string{}
ext := filepath.Ext(i.Name)
dir, err := os.Open(parentDir)
if err != nil {
// Directory must have been deleted, remove it from the index
return
}
// Read the directory contents
files, err := dir.Readdir(-1)
if err != nil {
return
}
ext := filepath.Ext(i.Path)
base := strings.TrimSuffix(i.Name, ext)
subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
for _, f := range files {
if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
continue
}
for _, subtitleExt := range subtitleExts {
if strings.HasSuffix(f.Name(), subtitleExt) {
i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
break
// detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name)
dir, err := afero.ReadDir(i.Fs, parentDir)
if err == nil {
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
}
}
}
}
// readListing reads the contents of a directory and fills the listing.
func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bool) error {
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
@@ -383,14 +311,13 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
listing := &Listing{
Items: []*FileInfo{},
Path: i.Path,
NumDirs: 0,
NumFiles: 0,
}
for _, f := range dir {
name := f.Name()
fPath := filepath.Join(i.Path, name)
fPath := path.Join(i.Path, name)
if !checker.Check(fPath) {
continue
@@ -399,6 +326,8 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead of the target's.
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info
@@ -408,16 +337,15 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
}
file := &FileInfo{
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
}
if f.IsDir() {
file.IsDir = true
}
if isSymlink {
file.IsSymlink = true
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
}
if file.IsDir {
@@ -428,7 +356,7 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
if isInvalidLink {
file.Type = "invalid_link"
} else {
err := file.detectType(path, true, false, readHeader)
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}
@@ -441,23 +369,3 @@ func (i *FileInfo) readListing(path string, checker rules.Checker, readHeader bo
i.Listing = listing
return nil
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}
func getMutex(path string) *sync.Mutex {
// Lock access to pathMutexes map
pathMutexesMu.Lock()
defer pathMutexesMu.Unlock()
// Create a mutex for the path if it doesn't exist
if pathMutexes[path] == nil {
pathMutexes[path] = &sync.Mutex{}
}
return pathMutexes[path]
}
-167
View File
@@ -1,167 +0,0 @@
package files
import (
"bytes"
"log"
"os"
"strings"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/settings"
)
type Directory struct {
Metadata map[string]FileInfo
Files string
}
type File struct {
Name string
IsDir bool
}
type Index struct {
Root string
Directories map[string]Directory
NumDirs int
NumFiles int
inProgress bool
quickList []File
LastIndexed time.Time
mu sync.RWMutex
}
var (
rootPath string = "/srv"
indexes []*Index
indexesMutex sync.RWMutex
)
func InitializeIndex(intervalMinutes uint32, schedule bool) {
if schedule {
go indexingScheduler(intervalMinutes)
}
}
func indexingScheduler(intervalMinutes uint32) {
if settings.Config.Server.Root != "" {
rootPath = settings.Config.Server.Root
}
si := GetIndex(rootPath)
log.Printf("Indexing Files...")
log.Printf("Configured to run every %v minutes", intervalMinutes)
log.Printf("Indexing from root: %s", si.Root)
for {
startTime := time.Now()
// Set the indexing flag to indicate that indexing is in progress
si.resetCount()
// Perform the indexing operation
err := si.indexFiles(si.Root)
si.quickList = []File{}
// Reset the indexing flag to indicate that indexing has finished
si.inProgress = false
// Update the LastIndexed time
si.LastIndexed = time.Now()
if err != nil {
log.Printf("Error during indexing: %v", err)
}
if si.NumFiles+si.NumDirs > 0 {
timeIndexedInSeconds := int(time.Since(startTime).Seconds())
log.Println("Successfully indexed files.")
log.Printf("Time spent indexing: %v seconds\n", timeIndexedInSeconds)
log.Printf("Files found: %v\n", si.NumFiles)
log.Printf("Directories found: %v\n", si.NumDirs)
}
// Sleep for the specified interval
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
}
}
// Define a function to recursively index files and directories
func (si *Index) indexFiles(path string) error {
// Check if the current directory has been modified since the last indexing
path = strings.TrimSuffix(path, "/")
adjustedPath := makeIndexPath(path, si.Root)
dir, err := os.Open(path)
if err != nil {
// Directory must have been deleted, remove it from the index
si.RemoveDirectory(adjustedPath)
}
dirInfo, err := dir.Stat()
if err != nil {
dir.Close()
return err
}
// Compare the last modified time of the directory with the last indexed time
lastIndexed := si.LastIndexed
if dirInfo.ModTime().Before(lastIndexed) {
dir.Close()
return nil
}
// Read the directory contents
files, err := dir.Readdir(-1)
if err != nil {
return err
}
dir.Close()
si.UpdateQuickList(files)
si.InsertFiles(path)
// done separately for memory efficiency on recursion
si.InsertDirs(path)
return nil
}
func (si *Index) InsertFiles(path string) {
adjustedPath := makeIndexPath(path, si.Root)
subDirectory := Directory{}
buffer := bytes.Buffer{}
for _, f := range si.GetQuickList() {
if !f.IsDir {
buffer.WriteString(f.Name + ";")
si.UpdateCount("files")
}
}
// Use GetMetadataInfo and SetFileMetadata for safer read and write operations
subDirectory.Files = buffer.String()
si.SetDirectoryInfo(adjustedPath, subDirectory)
}
func (si *Index) InsertDirs(path string) {
adjustedPath := makeIndexPath(path, si.Root)
for _, f := range si.GetQuickList() {
if f.IsDir {
if _, exists := si.Directories[adjustedPath]; exists {
si.UpdateCount("dirs")
// Add or update the directory in the map
if adjustedPath == "/" {
si.SetDirectoryInfo("/"+f.Name, Directory{})
} else {
si.SetDirectoryInfo(adjustedPath+"/"+f.Name, Directory{})
}
}
err := si.indexFiles(path + "/" + f.Name)
if err != nil {
if err.Error() == "invalid argument" {
log.Printf("Could not index \"%v\": %v \n", path, "Permission Denied")
} else {
log.Printf("Could not index \"%v\": %v \n", path, err)
}
}
}
}
}
func makeIndexPath(path string, root string) string {
if path == root {
return "/"
}
adjustedPath := strings.TrimPrefix(path, root+"/")
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
if adjustedPath == "" {
adjustedPath = "/"
}
return adjustedPath
}
-140
View File
@@ -1,140 +0,0 @@
package files
import (
"encoding/json"
"math/rand"
"reflect"
"testing"
"time"
"github.com/gtsteffaniak/filebrowser/settings"
)
func BenchmarkFillIndex(b *testing.B) {
InitializeIndex(5, false)
si := GetIndex(settings.Config.Server.Root)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
si.createMockData(50, 3) // 1000 dirs, 3 files per dir
}
}
func (si *Index) createMockData(numDirs, numFilesPerDir int) {
for i := 0; i < numDirs; i++ {
dirName := generateRandomPath(rand.Intn(3) + 1)
files := []File{}
// Append a new Directory to the slice
for j := 0; j < numFilesPerDir; j++ {
newFile := File{
Name: "file-" + getRandomTerm() + getRandomExtension(),
IsDir: false,
}
files = append(files, newFile)
}
si.UpdateQuickListForTests(files)
si.InsertFiles(dirName)
si.InsertDirs(dirName)
}
}
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("root"); !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) {
InitializeIndex(tt.args.intervalMinutes, false)
})
}
}
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)
})
}
}
+111
View File
@@ -0,0 +1,111 @@
package files
import (
"sort"
"strings"
"github.com/maruel/natural"
)
// Listing is a collection of files.
type Listing struct {
Items []*FileInfo `json:"items"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting Sorting `json:"sorting"`
}
// ApplySort applies the sort order using .Order and .Sort
//
//nolint:goconst
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc {
switch l.Sorting.By {
case "name":
sort.Sort(sort.Reverse(byName(l)))
case "size":
sort.Sort(sort.Reverse(bySize(l)))
case "modified":
sort.Sort(sort.Reverse(byModified(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sorting.By {
case "name":
sort.Sort(byName(l))
case "size":
sort.Sort(bySize(l))
case "modified":
sort.Sort(byModified(l))
default:
sort.Sort(byName(l))
return
}
}
}
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {
return len(l.Items)
}
func (l byName) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
if l.Items[i].IsDir && !l.Items[j].IsDir {
return l.Sorting.Asc
}
if !l.Items[i].IsDir && l.Items[j].IsDir {
return !l.Sorting.Asc
}
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
}
// By Size
func (l bySize) Len() int {
return len(l.Items)
}
func (l bySize) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
if l.Items[i].IsDir {
iSize = directoryOffset + iSize
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
}
return iSize < jSize
}
// By Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}
-174
View File
@@ -1,174 +0,0 @@
package files
import (
"math/rand"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
var (
sessionInProgress sync.Map
maxSearchResults = 100
)
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
// Remove slashes
scope = strings.TrimLeft(scope, "/")
scope = strings.TrimRight(scope, "/")
runningHash := generateRandomHash(4)
sessionInProgress.Store(sourceSession, runningHash) // Store the value in the sync.Map
searchOptions := ParseSearch(search)
fileListTypes := make(map[string]map[string]bool)
matching := []string{}
count := 0
for _, searchTerm := range searchOptions.Terms {
if searchTerm == "" {
continue
}
si.mu.Lock()
defer si.mu.Unlock()
for dirName, dir := range si.Directories {
isDir := true
files := strings.Split(dir.Files, ";")
value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash {
return []string{}, map[string]map[string]bool{}
}
if count > maxSearchResults {
break
}
pathName := scopedPathNameFilter(dirName, scope, isDir)
if pathName == "" {
continue // path not matched
}
fileTypes := map[string]bool{}
matches, fileType := containsSearchTerm(dirName, searchTerm, *searchOptions, isDir, fileTypes)
if matches {
fileListTypes[pathName] = fileType
matching = append(matching, pathName)
count++
}
isDir = false
for _, file := range files {
if file == "" {
continue
}
value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash {
return []string{}, map[string]map[string]bool{}
}
if count > maxSearchResults {
break
}
fullName := pathName + file
fileTypes := map[string]bool{}
matches, fileType := containsSearchTerm(fullName, searchTerm, *searchOptions, isDir, fileTypes)
if !matches {
continue
}
fileListTypes[fullName] = fileType
matching = append(matching, fullName)
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 {
pathName = strings.TrimLeft(pathName, "/")
pathName = strings.TrimRight(pathName, "/")
if strings.HasPrefix(pathName, scope) || scope == "" {
pathName = strings.TrimPrefix(pathName, scope)
pathName = strings.TrimLeft(pathName, "/")
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)
}
-296
View File
@@ -1,296 +0,0 @@
package files
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func BenchmarkSearchAllIndexes(b *testing.B) {
InitializeIndex(5, false)
si := GetIndex(rootPath)
si.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 {
si.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 TestSearchWhileIndexing(t *testing.T) {
InitializeIndex(5, false)
si := GetIndex(rootPath)
// Generate 100 random search terms
// Generate 100 random search terms
searchTerms := generateRandomSearchTerms(10)
for i := 0; i < 5; i++ {
// Execute the SearchAllIndexes function
go si.createMockData(100, 100) // 1000 dirs, 3 files per dir
for _, term := range searchTerms {
go si.Search(term, "/", "test")
}
}
}
func TestSearchIndexes(t *testing.T) {
index := Index{
Directories: map[string]Directory{
"test": {
Files: "audio1.wav;",
},
"test/path": {
Files: "file.txt;",
},
"new": {},
"new/test": {
Files: "audio.wav;video.mp4;video.MP4;",
},
"new/test/path": {
Files: "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/", "new/test/"},
expectedTypes: map[string]map[string]bool{
"test/": map[string]bool{"dir": true},
"new/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 := index.Search(tt.search, tt.scope, "")
assert.Equal(t, tt.expectedResult, actualResult)
if !reflect.DeepEqual(tt.expectedTypes, actualTypes) {
t.Fatalf("\n got: %+v\n want: %+v", actualTypes, tt.expectedTypes)
}
})
}
}
func Test_scopedPathNameFilter(t *testing.T) {
tests := []struct {
name string
args struct {
pathName string
scope string
isDir bool // Assuming isDir should be included in args
}
want string
}{
{
name: "scope test",
args: struct {
pathName string
scope string
isDir bool
}{
pathName: "/",
scope: "/",
isDir: false,
},
want: "", // Update this with the expected result
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := scopedPathNameFilter(tt.args.pathName, tt.args.scope, tt.args.isDir); got != tt.want {
t.Errorf("scopedPathNameFilter() = %v, want %v", got, tt.want)
}
})
}
}
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)
}
})
}
}
+7
View File
@@ -0,0 +1,7 @@
package files
// Sorting contains a sorting order.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}
-171
View File
@@ -1,171 +0,0 @@
package files
import (
"io/fs"
"log"
"time"
"github.com/gtsteffaniak/filebrowser/settings"
)
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
return FileInfo{}, false
} else {
return dir.Metadata[adjustedPath], true
}
}
return FileInfo{}, false
}
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
}
// Release the read lock before calling SetFileMetadata
}
return si.SetFileMetadata(adjustedPath, info)
}
// SetFileMetadata sets the FileInfo for the specified directory in the index.
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.Lock()
defer si.mu.Unlock()
_, exists := si.Directories[adjustedPath]
if !exists {
return false
}
info.CacheTime = time.Now()
si.Directories[adjustedPath].Metadata[adjustedPath] = info
return true
}
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
}
info, metadataExists := dir.Metadata[adjustedPath]
return info, metadataExists
}
return FileInfo{}, false
}
// SetDirectoryInfo sets the directory information in the index.
func (si *Index) SetDirectoryInfo(adjustedPath string, dir Directory) {
si.mu.Lock()
si.Directories[adjustedPath] = dir
si.mu.Unlock()
}
// SetDirectoryInfo sets the directory information in the index.
func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
return dir, true
}
return Directory{}, false
}
func (si *Index) RemoveDirectory(path string) {
si.mu.Lock()
defer si.mu.Unlock()
delete(si.Directories, path)
}
func (si *Index) UpdateCount(given string) {
si.mu.Lock()
defer si.mu.Unlock()
if given == "files" {
si.NumFiles++
} else if given == "dirs" {
si.NumDirs++
} else {
log.Println("could not update unknown type: ", given)
}
}
func (si *Index) resetCount() {
si.mu.Lock()
defer si.mu.Unlock()
si.NumDirs = 0
si.NumFiles = 0
si.inProgress = true
}
func GetIndex(root string) *Index {
for _, index := range indexes {
if index.Root == root {
return index
}
}
if settings.Config.Server.Root != "" {
rootPath = settings.Config.Server.Root
}
newIndex := &Index{
Root: rootPath,
Directories: make(map[string]Directory), // Initialize the map
NumDirs: 0,
NumFiles: 0,
inProgress: false,
}
indexesMutex.Lock()
indexes = append(indexes, newIndex)
indexesMutex.Unlock()
return newIndex
}
func (si *Index) UpdateQuickList(files []fs.FileInfo) {
si.mu.Lock()
defer si.mu.Unlock()
si.quickList = []File{}
for _, file := range files {
newFile := File{
Name: file.Name(),
IsDir: file.IsDir(),
}
si.quickList = append(si.quickList, newFile)
}
}
func (si *Index) UpdateQuickListForTests(files []File) {
si.mu.Lock()
defer si.mu.Unlock()
si.quickList = []File{}
for _, file := range files {
newFile := File{
Name: file.Name,
IsDir: file.IsDir,
}
si.quickList = append(si.quickList, newFile)
}
}
func (si *Index) GetQuickList() []File {
si.mu.Lock()
defer si.mu.Unlock()
newQuickList := si.quickList
return newQuickList
}
+59
View File
@@ -0,0 +1,59 @@
package files
import (
"os"
"unicode/utf8"
)
func isBinary(content []byte) bool {
maybeStr := string(content)
runeCnt := utf8.RuneCount(content)
runeIndex := 0
gotRuneErrCnt := 0
firstRuneErrIndex := -1
const (
// 8 and below are control chars (e.g. backspace, null, eof, etc)
maxControlCharsCode = 8
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
unicodeReplacementChar = 0xFFFD
)
for _, b := range maybeStr {
if b <= maxControlCharsCode {
return true
}
if b == unicodeReplacementChar {
// if it is not the last (utf8.UTFMax - x) rune
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
return true
}
// else it is the last (utf8.UTFMax - x) rune
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
gotRuneErrCnt++
// mark the first time
if firstRuneErrIndex == -1 {
firstRuneErrIndex = runeIndex
}
}
runeIndex++
}
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
return true
}
return false
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}
+6 -4
View File
@@ -11,6 +11,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-cmp v0.5.9
github.com/gorilla/mux v1.8.0
github.com/maruel/natural v1.1.0
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archiver/v3 v3.5.1
github.com/shirou/gopsutil/v3 v3.23.8
@@ -19,9 +20,10 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
golang.org/x/crypto v0.17.0
golang.org/x/crypto v0.12.0
golang.org/x/image v0.12.0
golang.org/x/text v0.14.0
golang.org/x/text v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
@@ -48,8 +50,8 @@ require (
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/bbolt v1.3.4 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+10 -8
View File
@@ -204,6 +204,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ=
github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
@@ -280,8 +282,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -357,8 +359,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -425,9 +427,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -440,9 +441,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -595,6 +595,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+41 -16
View File
@@ -2,7 +2,6 @@ package http
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
@@ -13,12 +12,27 @@ import (
"github.com/golang-jwt/jwt/v4/request"
"github.com/gtsteffaniak/filebrowser/errors"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users"
)
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
}
@@ -52,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
@@ -109,7 +123,7 @@ type signupBody struct {
}
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !settings.Config.Auth.Signup {
if !d.settings.Signup {
return http.StatusMethodNotAllowed, nil
}
@@ -129,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.Config.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)
@@ -139,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.Config.UserDefaults.Apply(user)
err = d.store.Users.Save(user)
if err == errors.ErrExist {
return http.StatusConflict, err
@@ -155,22 +175,27 @@ 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) {
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
if err != nil {
fmt.Println("Error parsing duration:", err)
duration = time.Hour * 2
}
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(duration)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
Issuer: "File Browser",
},
}
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
}
+4
View File
@@ -7,6 +7,7 @@ import (
"github.com/tomasen/realip"
"github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/runner"
"github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/storage"
@@ -26,6 +27,9 @@ type data struct {
// Check implements rules.Checker.
func (d *data) Check(path string) bool {
if d.user.HideDotfiles && rules.MatchHidden(path) {
return false
}
allow := true
for _, rule := range d.settings.Rules {
+3 -2
View File
@@ -45,7 +45,7 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
return http.StatusBadRequest, err
}
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: "/" + vars["path"],
Modify: d.user.Perm.Modify,
@@ -53,10 +53,10 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
setContentDisposition(w, r, file)
switch file.Type {
@@ -81,6 +81,7 @@ func handleImagePreview(
(previewSize == PreviewSizeThumb && !enableThumbnails) {
return rawFileHandler(w, r, file)
}
format, err := imgSvc.FormatFromExtension(file.Extension)
// Unsupported extensions directly return the raw data
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
+4 -4
View File
@@ -13,7 +13,6 @@ import (
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/share"
"github.com/gtsteffaniak/filebrowser/users"
)
var withHashFile = func(fn handleFunc) handleFunc {
@@ -36,7 +35,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
d.user = user
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: link.Path,
Modify: d.user.Perm.Modify,
@@ -63,7 +62,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
// set fs root to the shared file/folder
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
file, err = files.FileInfoFaster(files.FileOptions{
file, err = files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: filePath,
Modify: d.user.Perm.Modify,
@@ -99,7 +98,8 @@ var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Reques
file := d.raw.(*files.FileInfo)
if file.IsDir {
file.Listing.Sorting = users.Sorting{By: "name", Asc: false}
file.Listing.Sorting = files.Sorting{By: "name", Asc: false}
file.Listing.ApplySort()
return renderJSON(w, r, file)
}
+1 -5
View File
@@ -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)
}
+1 -1
View File
@@ -81,7 +81,7 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
return http.StatusAccepted, nil
}
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
+12 -5
View File
@@ -20,7 +20,7 @@ import (
)
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
@@ -32,10 +32,13 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
if err != nil {
return errToStatus(err), err
}
if file.IsDir {
file.Listing.Sorting = d.user.Sorting
file.Listing.ApplySort()
return renderJSON(w, r, file)
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
@@ -43,6 +46,9 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
} else if err != nil {
return http.StatusInternalServerError, err
}
// do not waste bandwidth if we just want the checksum
file.Content = ""
}
return renderJSON(w, r, file)
@@ -54,7 +60,7 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
return http.StatusForbidden, nil
}
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
@@ -96,7 +102,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err
}
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
@@ -302,7 +308,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
src = path.Clean("/" + src)
dst = path.Clean("/" + dst)
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
@@ -332,13 +338,14 @@ type DiskUsageResponse struct {
}
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.FileInfoFaster(files.FileOptions{
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
Checker: d,
Content: false,
})
if err != nil {
return errToStatus(err), err
+4 -8
View File
@@ -2,22 +2,18 @@ package http
import (
"net/http"
"strings"
"github.com/gtsteffaniak/filebrowser/files"
"github.com/gtsteffaniak/filebrowser/settings"
"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 := files.GetIndex(settings.Config.Server.Root)
combinedScope := strings.TrimPrefix(userScope+r.URL.Path, ".")
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,
+6 -5
View File
@@ -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.Config.Auth.Signup,
CreateUserDir: settings.Config.Server.CreateUserDir,
UserHomeBasePath: settings.Config.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
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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.Config.UserDefaults.DarkMode,
"Color": d.settings.Frontend.Color,
"BaseURL": d.server.BaseURL,
"Version": version.Version,
"StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": settings.Config.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,
+6 -5
View File
@@ -19,11 +19,6 @@ var (
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
)
// SortingSettings represents the sorting settings.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}
type modifyUserRequest struct {
modifyRequest
Data *users.User `json:"data"`
@@ -129,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)
@@ -184,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,45 +1,28 @@
package files
package search
import (
"mime"
"regexp"
"strconv"
"strings"
)
var typeRegexp = regexp.MustCompile(`type:(\S+)`)
var AllFiletypeOptions = []string{
"image",
"audio",
"archive",
"video",
"doc",
"dir",
"text",
}
var documentTypes = []string{
".word",
".pdf",
".txt",
".doc",
".docx",
}
var textTypes = []string{
".text",
".sh",
".yaml",
".yml",
".json",
".bashrc",
".zshrc",
".env",
}
var compressedFile = []string{
".7z",
".rar",
".zip",
".tar",
".gz",
".xz",
".tar.gz",
".tar.xz",
}
type SearchOptions struct {
@@ -137,48 +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 "pdf":
return extension == ".pdf"
case "text":
return isText(extension)
case "archive":
return isArchive(extension)
}
return false
}
func isText(extension string) bool {
for _, typefile := range textTypes {
if extension == typefile {
return true
}
}
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
}
+279
View File
@@ -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
}
+170
View File
@@ -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])
}
+19 -41
View File
@@ -3,23 +3,22 @@ package settings
import (
"log"
"os"
"strings"
"github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/users"
)
var Config Settings
var GlobalConfiguration Settings
func Initialize(configFile string) {
yamlData := loadConfigFile(configFile)
Config = setDefaults()
err := yaml.Unmarshal(yamlData, &Config)
GlobalConfiguration = setDefaults()
err := yaml.Unmarshal(yamlData, &GlobalConfiguration)
if err != nil {
log.Fatalf("Error unmarshaling YAML data: %v", err)
}
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
GlobalConfiguration.Server.Root = "/srv" // hardcoded for now. TODO allow changing
}
func loadConfigFile(configFile string) []byte {
@@ -27,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)
Config = setDefaults()
setDefaults()
return []byte{}
}
defer yamlFile.Close()
@@ -47,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,
@@ -60,46 +61,23 @@ func setDefaults() Settings {
Root: "/srv",
},
Auth: Auth{
TokenExpirationTime: "2h",
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
}
-59
View File
@@ -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)
}
})
}
}
+2 -2
View File
@@ -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)
-81
View File
@@ -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)
}
})
}
}
+9 -9
View File
@@ -12,14 +12,14 @@ func TestConfigLoadChanged(t *testing.T) {
yamlData := loadConfigFile("./testingConfig.yaml")
// Marshal the YAML data to a more human-readable format
newConfig := setDefaults()
Config := setDefaults()
GlobalConfiguration := setDefaults()
err := yaml.Unmarshal(yamlData, &newConfig)
if err != nil {
log.Fatalf("Error unmarshaling YAML data: %v", err)
}
// Use go-cmp to compare the two structs
if diff := cmp.Diff(newConfig, Config); diff == "" {
if diff := cmp.Diff(newConfig, GlobalConfiguration); diff == "" {
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
}
}
@@ -28,7 +28,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
yamlData := loadConfigFile("./testingConfig.yaml")
// Marshal the YAML data to a more human-readable format
newConfig := setDefaults()
Config := setDefaults()
GlobalConfiguration := setDefaults()
err := yaml.Unmarshal(yamlData, &newConfig)
if err != nil {
@@ -39,16 +39,16 @@ func TestConfigLoadSpecificValues(t *testing.T) {
globalVal interface{}
newVal interface{}
}{
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
{"Auth.Method", Config.Auth.Method, newConfig.Auth.Method},
{"Frontend.disableExternal", Config.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
{"UserDefaults.HideDotfiles", Config.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
{"Frontend.disableExternal", GlobalConfiguration.Frontend.DisableExternal, newConfig.Frontend.DisableExternal},
{"UserDefaults.HideDotfiles", GlobalConfiguration.UserDefaults.HideDotfiles, newConfig.UserDefaults.HideDotfiles},
{"Server.Database", GlobalConfiguration.Server.Database, newConfig.Server.Database},
}
for _, tc := range testCases {
if tc.globalVal == tc.newVal {
t.Errorf("Differences should have been found:\n\tConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
t.Errorf("Differences should have been found:\n\tGlobalConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
}
}
}
+5 -4
View File
@@ -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 -30
View File
@@ -5,28 +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 {
TokenExpirationTime string `json:"tokenExpirationTime"`
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 {
@@ -52,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 {
@@ -61,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"`
}
+1 -2
View File
@@ -28,13 +28,12 @@ frontend:
disableExternal: true
disableUsedPercentage: true
files: ""
theme: ""
color: ""
userDefaults:
scope: ""
locale: ""
viewMode: ""
darkMode: true
disableSettings: false
singleClick: true
sorting:
by: ""
+2 -2
View File
@@ -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 -3
View File
@@ -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
}
+65 -24
View File
@@ -6,9 +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"`
@@ -20,31 +27,23 @@ type Permissions struct {
Download bool `json:"download"`
}
// SortingSettings represents the sorting settings.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}
// 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 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.
@@ -52,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
+1 -1
View File
@@ -2,7 +2,7 @@ package version
var (
// Version is the current File Browser version.
Version = "(0.2.3)"
Version = "(0.2.0)"
// CommitSHA is the commmit sha.
CommitSHA = "(unknown)"
)
+63 -78
View File
@@ -8,8 +8,6 @@ Here is an expanded config file which includes all possible configurations:
```
server:
CreateUserDir: false
UserHomeBasePath: ""
indexingInterval: 5
numImageProcessors: 4
socket: ""
@@ -25,13 +23,10 @@ server:
database: "/database/database.db"
root: "/srv"
auth:
adminUsername: admin
adminPassword: admin
recaptcha:
host: ""
key: ""
secret: ""
tokenExpirationTime: 2h
header: ""
method: json
command: ""
@@ -42,12 +37,11 @@ frontend:
disableExternal: false
disableUsedPercentage: true
files: ""
theme: ""
color: ""
userDefaults:
settingsAllowed: true
darkMode: false
scope: ""
locale: "en"
locale: ""
viewMode: ""
singleClick: true
sorting:
@@ -70,37 +64,34 @@ userDefaults:
Here are the defaults if nothing is set:
```
server:
enableThumbnails: true
enableExec: false
indexingInterval: 5
port: 8080
numImageProcessors: 4
baseURL: ""
database: database.db
log: stdout
root: /srv
auth:
adminUsername: admin
adminPassword: admin
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
@@ -109,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.
@@ -121,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
@@ -156,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.
@@ -185,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`:
@@ -224,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`)
-16
View File
@@ -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
+10 -833
View File
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -39,10 +39,13 @@
"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": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
@@ -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

+147
View File
@@ -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

+5 -10
View File
@@ -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 ]}]
+220
View File
@@ -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;
}
+9 -5
View File
@@ -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>
-1
View File
@@ -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,
+45 -116
View File
@@ -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: {{ getContext }}
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: {{ getContext }}</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" },
@@ -549,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: "",
@@ -566,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";
@@ -601,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 == "";
},
@@ -631,13 +565,6 @@ export default {
searchHelp() {
return this.showHelp;
},
getContext() {
let path = this.$route.path
path = path.slice(1);
path = "./" + path.substring(path.indexOf("/") + 1);
path = path.replace(/\/+$/, "") + "/";
return path
},
},
mounted() {
window.addEventListener("resize", this.handleResize);
@@ -652,19 +579,20 @@ export default {
await this.$nextTick();
setTimeout(() => this.$router.push(url), 0);
},
basePath(str,isDir) {
let parts = str.replace(/(\/$|^\/)/, "").split("/");
if (parts.length <= 1) {
if (isDir) {
return "/"
}
return "";
getContext(url) {
url = url.slice(1);
let path = "./" + url.substring(url.indexOf("/") + 1);
return path.replace(/\/+$/, "") + "/";
},
basePath(str) {
let parts = str.split("/");
if (parts.length <= 2) {
return "/";
}
console.log("basePath",parts)
parts.pop();
parts = parts.join("/") + "/";
if (isDir) {
parts = "/" + parts; // fix weird rtl thing
if (str.endsWith("/")) {
parts = "/" + parts;
}
return parts;
},
@@ -695,6 +623,7 @@ export default {
return false;
}
this.searchTypes = this.searchTypes + string + " ";
},
resetSearchFilters() {
this.searchTypes = "";
+20 -64
View File
@@ -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);
},
+10 -82
View File
@@ -1,6 +1,5 @@
<template>
<div
:class="{ activebutton: this.isMaximized && this.isSelected}"
class="item"
role="button"
tabindex="0"
@@ -14,20 +13,15 @@
:aria-label="name"
:aria-selected="isSelected"
>
<div
@click="toggleClick"
:class="{ activetitle: this.isMaximized && this.isSelected }"
>
<img
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
v-lazy="thumbnailUrl"
:class="{ activeimg: this.isMaximized && this.isSelected }"
ref="thumbnail"
/>
<i :class="{ iconActive: this.isMaximized && this.isSelected }" v-else class="material-icons"></i>
<div>
<img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons"></i>
</div>
<div :class="{ activecontent: this.isMaximized && this.isSelected }">
<div>
<p class="name">{{ name }}</p>
<p v-if="isDir" class="size" data-order="-1">&mdash;</p>
@@ -40,27 +34,6 @@
</div>
</template>
<style>
.activebutton {
height: 10em;
}
.activecontent {
height: 5em !important;
display: grid !important;
}
.activeimg {
width: 8em !important;
height: 8em !important;
}
.iconActive {
font-size: 6em !important;
}
.activetitle {
width: 9em !important;
margin-right: 1em !important;
}
</style>
<script>
import { enableThumbs } from "@/utils/constants";
import { getHumanReadableFilesize } from "@/utils/filesizes";
@@ -73,8 +46,6 @@ export default {
name: "item",
data: function () {
return {
isThumbnailInView: false,
isMaximized: false,
touches: 0,
};
},
@@ -92,16 +63,6 @@ export default {
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
isClicked() {
if (this.user.singleClick || !this.allowedView ) {
return false;
}
// Assuming toggleClick returns a boolean value
return !this.isMaximized;
},
allowedView() {
return this.user.viewMode != "gallery" && this.user.viewMode != "normal"
},
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
@@ -123,12 +84,8 @@ export default {
return true;
},
thumbnailUrl() {
let path = this.req.path
if (this.req.path == "/") {
path = ""
}
const file = {
path: path +"/"+this.name,
path: this.path,
modified: this.modified,
};
@@ -137,41 +94,11 @@ export default {
isThumbsEnabled() {
return enableThumbs;
},
isInView() {
return enableThumbs;
},
},
mounted() {
const observer = new IntersectionObserver(this.handleIntersect, {
root: null,
rootMargin: '0px',
threshold: 0.5, // Adjust threshold as needed
});
// Get the thumbnail element and start observing
const thumbnailElement = this.$refs.thumbnail; // Add ref="thumbnail" to the <img> tag
if (thumbnailElement) {
observer.observe(thumbnailElement);
}
},
methods: {
handleIntersect(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.isThumbnailInView = true;
// Stop observing once thumbnail is in view
observer.unobserve(entry.target);
}
});
},
toggleClick() {
this.isMaximized = this.isClicked;
},
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () {
return this.type == "invalid_link"
? "invalid link"
: getHumanReadableFilesize(this.size);
return this.type == "invalid_link" ? "invalid link" : getHumanReadableFilesize(this.size);
},
humanTime: function () {
if (this.readOnly == undefined && this.user.dateFormat) {
@@ -304,6 +231,7 @@ export default {
return;
}
if (
!this.singleClick &&
!event.ctrlKey &&
+4 -1
View File
@@ -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,
+1 -1
View File
@@ -178,7 +178,7 @@ export default {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeUnmount() {
beforeDestroy() {
this.clip.destroy();
},
methods: {
+8 -10
View File
@@ -42,7 +42,7 @@
<languages
class="input input--block"
id="locale"
v-model:locale="user.locale"
:locale.sync="user.locale"
></languages>
</p>
@@ -55,13 +55,13 @@
{{ $t("settings.lockPassword") }}
</p>
<permissions :perm="user.perm" />
<commands v-if="isExecEnabled" v-model:commands="user.commands" />
<permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules v-model:rules="user.rules" />
<rules :rules.sync="user.rules" />
</div>
</div>
</template>
@@ -75,7 +75,7 @@ import { enableExec } from "@/utils/constants";
export default {
name: "user",
data() {
data: () => {
return {
createUserDirData: false,
originalUserScope: "/",
@@ -104,17 +104,15 @@ export default {
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir;
},
isExecEnabled() {
return enableExec; // Removed arrow function
},
isExecEnabled: () => enableExec,
},
watch: {
"user.perm.admin": function () {
if (!this.user.perm.admin) return;
this.user.lockPassword = false;
},
createUserDirData(newVal) {
this.user.scope = newVal ? "" : this.originalUserScope;
createUserDirData() {
this.user.scope = this.createUserDirData ? "" : this.originalUserScope;
},
},
};
@@ -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>
+1 -1
View File
@@ -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 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
}
-1
View File
@@ -203,7 +203,6 @@ body.rtl .breadcrumbs a {
width: 95%;
max-width: 30em;
z-index: 1;
border-radius: 1em;
}
button {
-278
View File
@@ -1,278 +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 {
color: white;
}
/* Result desktop */
.dark-mode #result-desktop #result-list {
max-height: unset;
}
/* Result desktop background */
.dark-mode #result-desktop {
background-color: var(--background);
}
+2 -2
View File
@@ -4,6 +4,7 @@
.dashboard .row {
display: flex;
margin: 0 -.5em;
flex-wrap: wrap;
justify-content: center;
}
@@ -165,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 {
@@ -459,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;
}
+28 -157
View File
@@ -1,7 +1,5 @@
#listing {
--item-selected: white;
transition: all;
animation-duration: 0.25s;
}
body.rtl #listing {
@@ -69,7 +67,6 @@ body.rtl #listing {
object-fit: cover;
margin-right: 0.1em;
vertical-align: bottom;
border-radius: 0.5em;
}
.message {
@@ -88,52 +85,41 @@ body.rtl #listing {
display: block;
}
#listing {
#listing.mosaic {
padding-top: 1em;
padding-bottom: 1em;
margin: 0 -0.5em;
}
#listing.gallery .item,
#listing.compact .item,
#listing.normal .item,
#listing.list .item {
#listing.mosaic .item {
width: calc(33% - 1em);
margin: .5em;
padding: 0.5em;
border-radius: 1em;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
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;
@@ -141,22 +127,21 @@ 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%;
border-radius: 0.5em;
}
#listing.gallery .size,
@@ -164,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%;
@@ -278,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;
@@ -298,7 +184,6 @@ body.rtl #listing {
#listing.list .item div:first-of-type img {
width: 2em;
height: 2em;
border-radius: 0.25em;
}
#listing.list .item div:last-of-type {
@@ -315,7 +200,7 @@ body.rtl #listing {
width: 25%;
}
#listing .header {
#listing .item.header {
display: none !important;
background-color: #ccc;
}
@@ -326,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;
}
@@ -361,7 +232,7 @@ body.rtl #listing {
color: inherit;
}
#listing.list .header>div:first-child {
#listing.list .item.header>div:first-child {
width: 0;
}
@@ -369,7 +240,7 @@ body.rtl #listing {
font-weight: normal;
}
#listing.list .header .name {
#listing.list .item.header .name {
margin-right: 3em;
}
@@ -387,7 +258,7 @@ body.rtl #listing {
opacity: 1;
}
#listing.list .header .active {
#listing.list .item.header .active {
font-weight: bold;
}
+1 -1
View File
@@ -1,5 +1,5 @@
#login {
background: var(--background);
background: #fff;
position: fixed;
top: 0;
left: 0;
+8 -7
View File
@@ -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 {
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%;
+1 -1
View File
@@ -152,7 +152,7 @@
"images": "الصور",
"music": "الموسيقى",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"pressToSearch": "No results found in indexed search.",
"search": "البحث...",
"typeToSearch": "Type to search...",
"types": "الأنواع",
+3 -8
View File
@@ -36,8 +36,7 @@
"toggleSidebar": "Seitenleiste anzeigen",
"update": "Update",
"upload": "Upload",
"openFile": "Datei öffnen",
"continue": "Fortfahren"
"openFile": "Datei öffnen"
},
"download": {
"downloadFile": "Download Datei",
@@ -148,7 +147,7 @@
"rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
"schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"show": "Anzeigen",
@@ -185,14 +184,10 @@
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"tusUploads": "Gestückelter Upload",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
"customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "Dokumentation",
"examples": "Beispiele",
"executeOnShell": "In Shell ausführen",
-282
View File
@@ -1,282 +0,0 @@
{
"buttons": {
"cancel": "Ακύρωση",
"close": "Κλείσιμο",
"copy": "Αντιγραφή",
"copyFile": "Αντιγραφή αρχείου",
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
"create": "Δημιουργία",
"delete": "Διαγραφή",
"download": "Λήψη",
"file": "Αρχείο",
"folder": "Φάκελος",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων",
"info": "Πληροφορίες",
"more": "Περισσότερα",
"move": "Μετακίνηση",
"moveFile": "Μετακίνηση αρχείου",
"new": "Νέο",
"next": "Επόμενο",
"ok": "Εντάξει",
"permalink": "Λήψη μόνιμου συνδέσμου",
"previous": "Προηγούμενο",
"publish": "Δημοσίευση",
"rename": "Μετονομασία",
"replace": "Αντικατάσταση",
"reportIssue": "Αναφορά προβλήματος",
"save": "Αποθήκευση",
"schedule": "Προγραμματισμός",
"search": "Αναζήτηση",
"select": "Επιλογή",
"selectMultiple": "Επιλογή πολλαπλών",
"share": "Κοινοποίηση",
"submit": "Υποβολή",
"switchView": "Εναλλαγή προβολής",
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
"update": "Ενημέρωση",
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου",
"continue": "Συνέχεια"
},
"download": {
"downloadFile": "Λήψη αρχείου",
"downloadFolder": "Λήψη φακέλου",
"downloadSelected": "Λήψη επιλεγμένων"
},
"upload": {
"abortUpload": "Είστε σίγουροι ότι θέλετε να διακόψετε τη μεταφόρτωση;"
},
"errors": {
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
"internal": "Προέκυψε εσωτερικό σφάλμα.",
"notFound": "Αυτή η τοποθεσία δεν μπορεί να βρεθεί.",
"connection": "Ο διακομιστής δεν είναι διαθέσιμος."
},
"files": {
"body": "Περιεχόμενο",
"clear": "Καθαρισμός",
"closePreview": "Κλείσιμο προεπισκόπησης",
"files": "Αρχεία",
"folders": "Φάκελοι",
"home": "Αρχική",
"lastModified": "Τελευταία τροποποίηση",
"loading": "Φορτώνει…",
"lonely": "Δεν υπάρχει τίποτα εδώ (ακόμη)…",
"metadata": "Μεταδεδομένα",
"multipleSelectionEnabled": "Ενεργοποιημένη επιλογή πολλαπλών",
"name": "Όνομα",
"size": "Μέγεθος",
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
"sortByName": "Ταξινόμηση κατά όνομα",
"sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
},
"help": {
"click": "επιλέξτε αρχείο ή φάκελο",
"ctrl": {
"click": "επιλογή πολλαπλών αρχείων ή φακέλων",
"f": "ανοίγει την αναζήτηση",
"s": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε"
},
"del": "διαγραφή επιλεγμένων στοιχείων",
"doubleClick": "ανοίγει ένα αρχείο ή φάκελο",
"esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο",
"f1": "αυτή η πληροφορία",
"f2": "μετονομασία αρχείου",
"help": "Βοήθεια"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"el": "Ελληνικά",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Δημιουργία λογαριασμού",
"loginInstead": "Έχετε ήδη λογαριασμό",
"password": "Κωδικός πρόσβασης",
"passwordConfirm": "Επιβεβαίωση κωδικού πρόσβασης",
"passwordsDontMatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"signup": "Εγγραφή",
"submit": "Είσοδος",
"username": "Όνομα χρήστη",
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης"
},
"permanent": "Μόνιμο",
"prompts": {
"copy": "Αντιγραφή",
"copyMessage": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
"deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε {count} αρχεία;",
"deleteMessageSingle": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το αρχείο/φάκελο;",
"deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κοινοποίηση ({path});",
"deleteTitle": "Διαγραφή αρχείων",
"displayName": "Εμφάνιση ονόματος:",
"download": "Λήψη αρχείων",
"downloadMessage": "Επιλέξτε τη μορφή που θέλετε να λάβετε.",
"error": "Προέκυψε κάποιο σφάλμα",
"fileInfo": "Πληροφορίες αρχείου",
"filesSelected": "Επιλέχθηκαν {count} αρχεία.",
"lastModified": "Τελευταία τροποποίηση",
"move": "Μετακίνηση",
"moveMessage": "Επιλέξτε νέα τοποθεσία για τα αρχεία / τους φακέλους σας:",
"newArchetype": "Δημιουργία νέας ανάρτησης με βάση έναν αρχέτυπο. Το αρχείο σας θα δημιουργηθεί στο φάκελο περιεχομένου.",
"newDir": "Νέος φάκελος",
"newDirMessage": "Γράψτε το όνομα του νέου φακέλου.",
"newFile": "Νέο αρχείο",
"newFileMessage": "Γράψτε το όνομα του νέου αρχείου.",
"numberDirs": "Αριθμός φακέλων",
"numberFiles": "Αριθμός αρχείων",
"rename": "Μετονομασία",
"renameMessage": "Εισαγάγετε ένα νέο όνομα για το",
"replace": "Αντικατάσταση",
"replaceMessage": "Ένα από τα αρχεία που προσπαθείτε να μεταφορτώσετε δημιουργεί σύγκρουση με υπάρχον αρχείο λόγω του ονόματός του. Θέλετε να συνεχίσετε τη μεταφόρτωση ή να αντικαταστήσετε το υπάρχον;\n",
"schedule": "Προγραμματισμός",
"scheduleMessage": "Επιλέξτε μια ημερομηνία και ώρα για τον προγραμματισμό της δημοσίευσης αυτής της ανάρτησης.",
"show": "Εμφάνιση",
"size": "Μέγεθος",
"upload": "Μεταφόρτωση",
"uploadFiles": "Μεταφόρτωση {files} αρχείων…",
"uploadMessage": "Επιλέξτε μια επιλογή για τη μεταφόρτωση.",
"optionalPassword": "Προαιρετικός κωδικός πρόσβασης"
},
"search": {
"images": "Εικόνες",
"music": "Μουσική",
"pdf": "PDF",
"pressToSearch": "Πατήστε Enter για αναζήτηση…",
"search": "Αναζήτηση…",
"typeToSearch": "Πληκτρολογήστε για αναζήτηση…",
"types": "Τύποι",
"video": "Βίντεο"
},
"settings": {
"admin": "Διαχειριστής",
"administrator": "Διαχειριστής",
"allowCommands": "Εκτέλεση εντολών",
"allowEdit": "Επεξεργασία, μετονομασία και διαγραφή αρχείων ή φακέλων",
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
"brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
"changePassword": "Αλλαγή κωδικού πρόσβασης",
"commandRunner": "Εκτέλεση εντολών",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
"tusUploads": "Τμηματικές μεταφορές αρχείων",
"tusUploadsHelp": "Η εφαρμογή File Browser υποστηρίζει τμηματικές μεταφορτώσεις αρχείων, επιτρέποντας την αποδοτική, αξιόπιστη και συνεχιζόμενη μεταφόρτωση αρχείων ακόμα και σε ασταθείς συνδέσεις δικτύου.",
"tusUploadsChunkSize": "Υποδεικνύει το μέγιστο μέγεθος ενός αιτήματος μεταφόρτωσης (για μικρότερες μεταφορές αρχείων θα χρησιμοποιηθούν απευθείας και όχι τμηματικές μεταφορτώσεις). Μπορείτε να εισάγετε έναν ακέραιο αριθμό που υποδηλώνει το μέγεθος σε bytes, ή κείμενο με αριθμό και μονάδα μέτρησης μεγέθους δεδομένων, όπως 10MB, 1GB κλπ.",
"tusUploadsRetryCount": "Αριθμός επαναληπτικών δοκιμών που θα πραγματοποιηθούν αν αποτύχει η μεταφόρτωση ενός τμήματος.",
"userHomeBasePath": "Βασική διαδρομή αρχείων για τους φακέλους των χρηστών",
"userScopeGenerationPlaceholder": "Η εμβέλεια εφαρμογής θα δημιουργηθεί αυτόματα",
"createUserHomeDirectory": "Δημιουργία φακέλου χρήστη",
"customStylesheet": "Προσαρμοσμένο στυλ εμφάνισης (stylesheet)",
"defaultUserDescription": "Αυτές είναι οι προεπιλεγμένες ρυθμίσεις για νέους χρήστες.",
"disableExternalLinks": "Απενεργοποίηση εξωτερικών συνδέσμων (εκτός από συνδέσμους προς τις οδηγίες χρήσης)",
"disableUsedDiskPercentage": "Απενεργοποίηση γραφήματος ποσοστού χρήσης χώρου αποθήκευσης",
"documentation": "οδηγίες χρήσης",
"examples": "Παραδείγματα",
"executeOnShell": "Εκτέλεση στο κέλυφος",
"executeOnShellDescription": "Από προεπιλογή, η εφαρμογή File Browser εκτελεί τις εντολές καλώντας τα προγράμματα των εντολών απευθείας. Αν θέλετε να τις εκτελέσετε σε ένα κέλυφος (όπως το Bash ή το PowerShell), μπορείτε να το καθορίσετε εδώ με τις απαιτούμενες παραμέτρους. Εάν οριστεί, η εντολή που εκτελείτε θα προστίθεται ως παράμετρος. Αυτό ισχύει τόσο για τις εντολές χρήστη όσο και για τους αγκίστρους συμβάντων (event hooks).",
"globalRules": "Πρόκειται για ένα γενικό σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες και ισχύουν για κάθε χρήστη. Μπορείτε να καθορίσετε συγκεκριμένους κανόνες στις ρυθμίσεις κάθε χρήστη για να παρακάμψετε τους γενικούς κανόνες.",
"globalSettings": "Γενικές ρυθμίσεις",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων (dotfiles)",
"insertPath": "Εισάγετε διαδρομή",
"insertRegex": "Εισάγετε έκφραση regex",
"instanceName": "Όνομα περιβάλλοντος",
"language": "Γλώσσα",
"lockPassword": "Αποτρέψτε τον χρήστη από την αλλαγή του κωδικού πρόσβασης",
"newPassword": "Νέος κωδικός πρόσβασης",
"newPasswordConfirm": "Επιβεβαιώστε τον νέο κωδικό πρόσβασης",
"newUser": "Νέος χρήστης",
"password": "Κωδικός πρόσβασης",
"passwordUpdated": "Ο κωδικός πρόσβασης ενημερώθηκε!",
"path": "Διαδρομή",
"perm": {
"create": "Δημιουργία αρχείων και φακέλων",
"delete": "Διαγραφή αρχείων και φακέλων",
"download": "Λήψη",
"execute": "Εκτέλεση εντολών",
"modify": "Επεξεργασία αρχείων",
"rename": "Μετονομασία ή μετακίνηση αρχείων και φακέλων",
"share": "Κοινοποίηση αρχείων"
},
"permissions": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
"rules": "Κανόνες",
"rulesHelp": "Εδώ μπορείτε να ορίσετε ένα σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες για τον συγκεκριμένο χρήστη. Τα αποκλεισμένα αρχεία δεν θα εμφανίζονται στα περιεχόμενα των αντίστοιχων φακέλων και δεν θα είναι προσβάσιμα από τον χρήστη. Υποστηρίζονται εκφράσεις regex και διαδρομές σχετικές με την εμβέλεια αρχείων των χρηστών.\n",
"scope": "Εμβέλεια",
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
"settingsUpdated": "Οι ρυθμίσεις ενημερώθηκαν!",
"shareDuration": "Διάρκεια κοινοποίησης",
"shareManagement": "Διαχείριση κοινοποίησης",
"shareDeleted": "Η κοινοποίηση διαγράφηκε!",
"singleClick": "Χρήση μονού κλικ για να ανοίξετε αρχεία και φακέλους",
"themes": {
"dark": "Σκοτεινό",
"light": "Φωτεινό",
"title": "Μοτίβο"
},
"user": "Χρήστης",
"userCommands": "Εντολές χρήστη",
"userCommandsHelp": "Μια λίστα με τις διαθέσιμες εντολές για αυτόν το χρήστη, χωρισμένες μεταξύ τους με κενά. Παράδειγμα:\n",
"userCreated": "Ο χρήστης δημιουργήθηκε!",
"userDefaults": "Προεπιλεγμένες ρυθμίσεις χρήστη",
"userDeleted": "Ο χρήστης διαγράφηκε!",
"userManagement": "Διαχείριση χρηστών",
"userUpdated": "Ο χρήστης ενημερώθηκε!",
"username": "Όνομα χρήστη",
"users": "Χρήστες"
},
"sidebar": {
"help": "Βοήθεια",
"hugoNew": "Νέο Hugo",
"login": "Σύνδεση",
"logout": "Αποσύνδεση",
"myFiles": "Τα αρχεία μου",
"newFile": "Νέο αρχείο",
"newFolder": "Νέος φάκελος",
"preview": "Προεπισκόπηση",
"settings": "Ρυθμίσεις",
"signup": "Εγγραφή",
"siteSettings": "Ρυθμίσεις ιστότοπου"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
},
"time": {
"days": "Ημέρες",
"hours": "Ώρες",
"minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου"
}
}
+6 -15
View File
@@ -37,17 +37,13 @@
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload",
"openFile": "Open file",
"continue": "Continue"
"openFile": "Open file"
},
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder",
"downloadSelected": "Download Selected"
},
"upload": {
"abortUpload": "Are you sure you want to abort?"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
"internal": "Something really went wrong.",
@@ -92,7 +88,6 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -153,7 +148,7 @@
"rename": "Rename",
"renameMessage": "Insert a new name for",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n",
"schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"show": "Show",
@@ -169,7 +164,7 @@
"pdf": "PDF",
"pressToSearch": "No results found in indexed search.",
"search": "Search...",
"typeToSearch": "Type to search... (3 character minimum)",
"typeToSearch": "Type to search... (3 characters minimum)",
"types": "Types",
"video": "Video"
},
@@ -190,22 +185,18 @@
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "Commands updated!",
"createUserDir": "Auto create user home dir while adding new user",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting a bytes input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory",
"customStylesheet": "Custom Stylesheet",
"defaultUserDescription": "These are the default settings for new users.",
"defaultUserDescription": "This are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "documentation",
"examples": "Examples",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override these ones.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"globalSettings": "Global Settings",
"hideDotfiles": "Hide dotfiles",
"insertPath": "Insert the path",
@@ -281,4 +272,4 @@
"seconds": "Seconds",
"unit": "Time Unit"
}
}
}
-5
View File
@@ -5,7 +5,6 @@ import he from "./he.json";
import hu from "./hu.json";
import ar from "./ar.json";
import de from "./de.json";
import el from "./el.json";
import en from "./en.json";
import es from "./es.json";
import fr from "./fr.json";
@@ -39,9 +38,6 @@ export function detectLocale() {
case /^ar.*/i.test(locale):
locale = "ar";
break;
case /^el.*/i.test(locale):
locale = "el";
break;
case /^es.*/i.test(locale):
locale = "es";
break;
@@ -118,7 +114,6 @@ const i18n = new VueI18n({
hu: removeEmpty(hu),
ar: removeEmpty(ar),
de: removeEmpty(de),
el: removeEmpty(el),
en: en,
es: removeEmpty(es),
fr: removeEmpty(fr),
+1 -1
View File
@@ -152,7 +152,7 @@
"images": "画像",
"music": "音楽",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"pressToSearch": "No results found in indexed search.",
"search": "検索...",
"typeToSearch": "Type to search...",
"types": "種類",
-1
View File
@@ -8,7 +8,6 @@ import Vue from "@/utils/vue";
import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth";
import App from "@/App";
export const eventBus = new Vue(); // creating an event bus.
cssVars();
+1 -7
View File
@@ -7,14 +7,8 @@ import upload from "./modules/upload";
Vue.use(Vuex);
const state = {
editor: null,
user: null,
req: {
sorting: {
by: 'name', // Initial sorting field
asc: true, // Initial sorting order
},
},
req: {},
oldReq: {},
clipboard: {
key: "",
-19
View File
@@ -74,25 +74,6 @@ const mutations = {
state.oldReq = state.req;
state.req = value;
},
// Inside your mutations object
updateListingSortConfig(state, { field, asc }) {
state.req.sorting.by = field;
state.req.sorting.asc = asc;
},
updateListingItems(state) {
// Sort the items array based on the sorting settings
state.req.items.sort((a, b) => {
const valueA = a[state.req.sorting.by];
const valueB = b[state.req.sorting.by];
if (state.req.sorting.asc) {
return valueA > valueB ? 1 : -1;
} else {
return valueA < valueB ? 1 : -1;
}
});
},
updateClipboard: (state, value) => {
state.clipboard.key = value.key;
state.clipboard.items = value.items;
+3
View File
@@ -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 -4
View File
@@ -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
};
+10 -1
View File
@@ -100,6 +100,15 @@ export function scanFiles(dt) {
});
}
function detectType(mimetype) {
if (mimetype.startsWith("video")) return "video";
if (mimetype.startsWith("audio")) return "audio";
if (mimetype.startsWith("image")) return "image";
if (mimetype.startsWith("pdf")) return "pdf";
if (mimetype.startsWith("text")) return "text";
return "blob";
}
export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let id = store.state.upload.id;
@@ -121,7 +130,7 @@ export function handleFiles(files, base, overwrite = false) {
path,
file,
overwrite,
...(!file.isDir && { type: file.type }),
...(!file.isDir && { type: detectType(file.type) }),
};
store.dispatch("upload/upload", item);
+2
View File
@@ -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: {
+7 -7
View File
@@ -1,6 +1,7 @@
<template>
<div>
<breadcrumbs base="/files" />
<errors v-if="error" :errorCode="error.status" />
<component v-else-if="currentView" :is="currentView"></component>
<div v-else>
@@ -23,9 +24,8 @@ import { mapState, mapMutations } from "vuex";
import HeaderBar from "@/components/header/HeaderBar";
import Breadcrumbs from "@/components/Breadcrumbs";
import Errors from "@/views/Errors";
import Preview from "@/views/files/Preview.vue";
import Listing from "@/views/files/Listing.vue";
import Editor from "@/views/files/Editor.vue";
import Preview from "@/views/files/Preview";
import Listing from "@/views/files/Listing";
function clean(path) {
return path.endsWith("/") ? path.slice(0, -1) : path;
@@ -39,7 +39,7 @@ export default {
Errors,
Preview,
Listing,
Editor,
Editor: () => import("@/views/files/Editor"),
},
data: function () {
return {
@@ -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"]),
+20 -49
View File
@@ -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,42 +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>
main {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
main::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
/* 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
View File
@@ -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 {
+3 -6
View File
@@ -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>
+6 -2
View File
@@ -160,15 +160,19 @@ 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";
import Item from "@/components/files/ListingItem.vue";
import Item from "@/components/files/ListingItem";
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();
},
+22 -10
View File
@@ -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 () {
+9 -5
View File
@@ -2,6 +2,7 @@
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title class="topTitle">{{ req.name }}</title>
<action v-if="user.perm.modify" id="save-button" icon="save" :label="$t('buttons.save')"
@action="save()" />
</header-bar>
@@ -23,19 +24,20 @@
<script>
import { mapState } from "vuex";
import { eventBus } from "@/main";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
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: "editorBar",
name: "editor",
components: {
HeaderBar,
Action,
Breadcrumbs,
},
data: function () {
return {};
@@ -75,8 +77,9 @@ export default {
created() {
window.addEventListener("keydown", this.keyEvent);
},
beforeUnmount() {
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
this.editor.destroy();
},
methods: {
back() {
@@ -98,8 +101,9 @@ export default {
async save() {
const button = "save";
buttons.loading("save");
try {
eventBus.$emit("handleEditorValueRequest", "data");
await api.put(this.$route.path, this.editor.getValue());
buttons.success(button);
} catch (e) {
buttons.done(button);
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more