Merge branch 'v0.2.1'
This commit is contained in:
commit
aab3224d2c
|
@ -4,8 +4,9 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -13,7 +14,16 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go test -race -v ./...
|
- run: cd backend && go test -race -v ./...
|
||||||
lint:
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
|
||||||
|
- run: cd backend && golangci-lint run
|
||||||
|
format-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -21,8 +31,18 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go fmt ./...
|
- run: cd backend && go fmt ./...
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- run: cd frontend && npm i eslint
|
||||||
|
- run: cd frontend && npm run lint
|
||||||
|
|
||||||
push_latest_to_registry:
|
push_latest_to_registry:
|
||||||
needs: [lint, test]
|
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||||
name: Push latest
|
name: Push latest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -4,9 +4,10 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
- 'v\d+.\d+.\d+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -14,7 +15,16 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go test -race -v ./...
|
- run: cd backend && go test -race -v ./...
|
||||||
lint:
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
|
||||||
|
- run: cd backend && golangci-lint run
|
||||||
|
format-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -22,8 +32,18 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go fmt ./...
|
- run: cd backend && go fmt ./...
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- run: cd frontend && npm i eslint
|
||||||
|
- run: cd frontend && npm run lint
|
||||||
|
|
||||||
push_pr_to_registry:
|
push_pr_to_registry:
|
||||||
needs: [lint, test]
|
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||||
name: Push PR
|
name: Push PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -3,9 +3,9 @@ name: release
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'v\d+.\d+.\d+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -13,7 +13,16 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go test -race -v ./...
|
- run: cd backend && go test -race -v ./...
|
||||||
lint:
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.21.1
|
||||||
|
- run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2
|
||||||
|
- run: cd backend && golangci-lint run
|
||||||
|
format-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -21,11 +30,19 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.1
|
go-version: 1.21.1
|
||||||
- run: cd backend && go fmt ./...
|
- run: cd backend && go fmt ./...
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- run: cd frontend && npm i eslint
|
||||||
|
- run: cd frontend && npm run lint
|
||||||
push_release_to_registry:
|
push_release_to_registry:
|
||||||
needs: [lint, test]
|
needs: [lint-frontend, lint-backend, test-backend, format-backend]
|
||||||
name: Push release
|
name: Push release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -50,6 +67,13 @@ jobs:
|
||||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
with:
|
with:
|
||||||
images: gtstef/filebrowser
|
images: gtstef/filebrowser
|
||||||
|
- name: Strip v from version number
|
||||||
|
id: modify-json
|
||||||
|
run: |
|
||||||
|
JSON="${{ steps.meta.outputs.tags }}"
|
||||||
|
# Use jq to remove 'v' from the version field
|
||||||
|
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||||
|
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
@ -57,5 +81,5 @@ jobs:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
12
Dockerfile
12
Dockerfile
|
@ -1,7 +1,7 @@
|
||||||
FROM node:slim as nbuild
|
FROM node:slim as nbuild
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./frontend/package*.json ./
|
COPY ./frontend/package*.json ./
|
||||||
RUN npm i
|
RUN npm ci --maxsockets 1
|
||||||
COPY ./frontend/ ./
|
COPY ./frontend/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
@ -12,13 +12,11 @@ RUN go get -u golang.org/x/net
|
||||||
RUN go build -ldflags="-w -s" -o filebrowser .
|
RUN go build -ldflags="-w -s" -o filebrowser .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
ARG app="/app/filebrowser"
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
mailcap
|
mailcap
|
||||||
VOLUME /srv
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
COPY --from=base $app* ./
|
||||||
WORKDIR /
|
COPY --from=nbuild /app/dist/ ./frontend/dist/
|
||||||
COPY --from=base /app/filebrowser.yaml /filebrowser.yaml
|
|
||||||
COPY --from=base /app/filebrowser /filebrowser
|
|
||||||
COPY --from=nbuild /app/dist/ /frontend/dist/
|
|
||||||
ENTRYPOINT [ "./filebrowser" ]
|
ENTRYPOINT [ "./filebrowser" ]
|
|
@ -146,14 +146,11 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if u == nil {
|
if u == nil {
|
||||||
pass, err := users.HashPwd(a.Cred.Password)
|
log.Println("creds", a.Cred.Password)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// create user with the provided credentials
|
// create user with the provided credentials
|
||||||
d := &users.User{
|
d := &users.User{
|
||||||
Username: a.Cred.Username,
|
Username: a.Cred.Username,
|
||||||
Password: pass,
|
Password: a.Cred.Password,
|
||||||
Scope: a.Settings.UserDefaults.Scope,
|
Scope: a.Settings.UserDefaults.Scope,
|
||||||
Locale: a.Settings.UserDefaults.Locale,
|
Locale: a.Settings.UserDefaults.Locale,
|
||||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||||
|
@ -178,16 +175,6 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
}
|
}
|
||||||
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||||
u = a.GetUser(u)
|
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
|
// update user with provided fields
|
||||||
err := a.Users.Update(u)
|
err := a.Users.Update(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -201,31 +188,31 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
// GetUser returns a User filled with hook values or provided defaults
|
// GetUser returns a User filled with hook values or provided defaults
|
||||||
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||||
// adds all permissions when user is admin
|
// adds all permissions when user is admin
|
||||||
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
|
isAdmin := d.Perm.Admin
|
||||||
perms := users.Permissions{
|
perms := users.Permissions{
|
||||||
Admin: isAdmin,
|
Admin: isAdmin,
|
||||||
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
|
Execute: isAdmin || d.Perm.Execute,
|
||||||
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
|
Create: isAdmin || d.Perm.Create,
|
||||||
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
|
Rename: isAdmin || d.Perm.Rename,
|
||||||
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
|
Modify: isAdmin || d.Perm.Modify,
|
||||||
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
|
Delete: isAdmin || d.Perm.Delete,
|
||||||
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
|
Share: isAdmin || d.Perm.Share,
|
||||||
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
Download: isAdmin || d.Perm.Download,
|
||||||
}
|
}
|
||||||
user := users.User{
|
user := users.User{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
Username: d.Username,
|
Username: d.Username,
|
||||||
Password: d.Password,
|
Password: d.Password,
|
||||||
Scope: a.Fields.GetString("user.scope", d.Scope),
|
Scope: d.Scope,
|
||||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
Locale: d.Locale,
|
||||||
ViewMode: d.ViewMode,
|
ViewMode: d.ViewMode,
|
||||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
SingleClick: d.SingleClick,
|
||||||
Sorting: files.Sorting{
|
Sorting: files.Sorting{
|
||||||
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
Asc: d.Sorting.Asc,
|
||||||
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
By: d.Sorting.By,
|
||||||
},
|
},
|
||||||
Commands: a.Fields.GetArray("user.commands", d.Commands),
|
Commands: d.Commands,
|
||||||
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
|
HideDotfiles: d.HideDotfiles,
|
||||||
Perm: perms,
|
Perm: perms,
|
||||||
LockPassword: true,
|
LockPassword: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -17,22 +16,16 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/auth"
|
"github.com/gtsteffaniak/filebrowser/auth"
|
||||||
"github.com/gtsteffaniak/filebrowser/diskcache"
|
"github.com/gtsteffaniak/filebrowser/diskcache"
|
||||||
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
fbhttp "github.com/gtsteffaniak/filebrowser/http"
|
||||||
"github.com/gtsteffaniak/filebrowser/img"
|
"github.com/gtsteffaniak/filebrowser/img"
|
||||||
"github.com/gtsteffaniak/filebrowser/search"
|
"github.com/gtsteffaniak/filebrowser/index"
|
||||||
"github.com/gtsteffaniak/filebrowser/settings"
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/storage"
|
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
configFile string
|
|
||||||
)
|
|
||||||
|
|
||||||
type dirFS struct {
|
type dirFS struct {
|
||||||
http.Dir
|
http.Dir
|
||||||
}
|
}
|
||||||
|
@ -71,7 +64,7 @@ var rootCmd = &cobra.Command{
|
||||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
}
|
}
|
||||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||||
go search.InitializeIndex(serverConfig.IndexingInterval)
|
go index.Initialize(serverConfig.IndexingInterval)
|
||||||
_, err := os.Stat(serverConfig.Root)
|
_, err := os.Stat(serverConfig.Root)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
|
@ -124,41 +117,17 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
|
||||||
os.Exit(0)
|
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) {
|
func quickSetup(d pythonData) {
|
||||||
settings.GlobalConfiguration.Key = generateKey()
|
settings.GlobalConfiguration.Key = generateKey()
|
||||||
var err error
|
|
||||||
if settings.GlobalConfiguration.Auth.Method == "noauth" {
|
if settings.GlobalConfiguration.Auth.Method == "noauth" {
|
||||||
err = d.store.Auth.Save(&auth.NoAuth{})
|
err := d.store.Auth.Save(&auth.NoAuth{})
|
||||||
|
checkErr(err)
|
||||||
} else {
|
} else {
|
||||||
settings.GlobalConfiguration.Auth.Method = "password"
|
settings.GlobalConfiguration.Auth.Method = "password"
|
||||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
err := d.store.Auth.Save(&auth.JSONAuth{})
|
||||||
|
checkErr(err)
|
||||||
}
|
}
|
||||||
err = d.store.Settings.Save(&settings.GlobalConfiguration)
|
err := d.store.Settings.Save(&settings.GlobalConfiguration)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
|
err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
@ -168,12 +137,26 @@ func quickSetup(d pythonData) {
|
||||||
log.Fatal("username and password cannot be empty during quick setup")
|
log.Fatal("username and password cannot be empty during quick setup")
|
||||||
}
|
}
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
LockPassword: false,
|
|
||||||
}
|
}
|
||||||
user.Perm.Admin = true
|
user.Perm.Admin = true
|
||||||
|
<<<<<<< HEAD
|
||||||
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||||
|
=======
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
>>>>>>> v0.2.1
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,15 @@ var usersAddCmd = &cobra.Command{
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
|
<<<<<<< HEAD
|
||||||
password, err := users.HashPwd(args[1])
|
password, err := users.HashPwd(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> v0.2.1
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: args[0],
|
Username: args[0],
|
||||||
Password: password,
|
Password: args[1],
|
||||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
||||||
}
|
}
|
||||||
servSettings, err := d.store.Settings.GetServer()
|
servSettings, err := d.store.Settings.GetServer()
|
||||||
|
|
|
@ -8,9 +8,12 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
usersCmd.AddCommand(usersUpdateCmd)
|
usersCmd.AddCommand(usersUpdateCmd)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
|
usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
|
||||||
usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
|
usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
|
||||||
|
=======
|
||||||
|
>>>>>>> v0.2.1
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersUpdateCmd = &cobra.Command{
|
var usersUpdateCmd = &cobra.Command{
|
||||||
|
@ -21,9 +24,6 @@ options you want to change.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
flags := cmd.Flags()
|
|
||||||
password := mustGetString(flags, "password")
|
|
||||||
newUsername := mustGetString(flags, "username")
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
|
@ -36,23 +36,6 @@ options you want to change.`,
|
||||||
user, err = d.store.Users.Get("", username)
|
user, err = d.store.Users.Get("", username)
|
||||||
}
|
}
|
||||||
checkErr(err)
|
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)
|
err = d.store.Users.Update(user)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
|
@ -3,11 +3,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
|
@ -152,42 +150,3 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||||
return errors.New("invalid format: " + ext)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +1,31 @@
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
baseURL: "/"
|
baseURL: "/"
|
||||||
|
root: "/Users/steffag/git/go"
|
||||||
auth:
|
auth:
|
||||||
method: noauth
|
method: password
|
||||||
signup: true
|
signup: true
|
||||||
|
<<<<<<< HEAD
|
||||||
frontend:
|
frontend:
|
||||||
theme: dark
|
theme: dark
|
||||||
users:
|
users:
|
||||||
- name: admin
|
- name: admin
|
||||||
settings:
|
settings:
|
||||||
hideDotfiles: true
|
hideDotfiles: true
|
||||||
singleClick: false
|
singleClick: false
|
||||||
|
=======
|
||||||
|
userDefaults:
|
||||||
|
darkMode: true
|
||||||
|
disableSettings: false
|
||||||
|
scope: "."
|
||||||
|
hideDotfiles: true
|
||||||
|
singleClick: false
|
||||||
|
permissions:
|
||||||
|
admin: false
|
||||||
|
create: true
|
||||||
|
rename: true
|
||||||
|
modify: true
|
||||||
|
delete: true
|
||||||
|
share: true
|
||||||
|
download: true
|
||||||
|
>>>>>>> v0.2.1
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v4/request"
|
"github.com/golang-jwt/jwt/v4/request"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"github.com/gtsteffaniak/filebrowser/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,20 +20,8 @@ const (
|
||||||
TokenExpirationTime = time.Hour * 2
|
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 {
|
type authToken struct {
|
||||||
User userInfo `json:"user"`
|
User users.User `json:"user"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +132,9 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||||
|
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: info.Username,
|
Username: info.Username,
|
||||||
|
Password: info.Password,
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
pwd, err := users.HashPwd(info.Password)
|
pwd, err := users.HashPwd(info.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -152,6 +143,9 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
|
||||||
|
|
||||||
user.Password = pwd
|
user.Password = pwd
|
||||||
|
|
||||||
|
=======
|
||||||
|
settings.GlobalConfiguration.UserDefaults.Apply(user)
|
||||||
|
>>>>>>> v0.2.1
|
||||||
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||||
|
@ -176,17 +170,7 @@ 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) {
|
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||||
claims := &authToken{
|
claims := &authToken{
|
||||||
User: userInfo{
|
User: *user,
|
||||||
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{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
|
||||||
|
|
|
@ -3,17 +3,17 @@ package http
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/search"
|
"github.com/gtsteffaniak/filebrowser/index"
|
||||||
)
|
)
|
||||||
|
|
||||||
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
response := []map[string]interface{}{}
|
response := []map[string]interface{}{}
|
||||||
query := r.URL.Query().Get("query")
|
query := r.URL.Query().Get("query")
|
||||||
|
|
||||||
// Retrieve the User-Agent and X-Auth headers from the request
|
// Retrieve the User-Agent and X-Auth headers from the request
|
||||||
sessionId := r.Header.Get("SessionId")
|
sessionId := r.Header.Get("SessionId")
|
||||||
indexInfo, fileTypes := search.SearchAllIndexes(query, r.URL.Path, sessionId)
|
index := *index.GetIndex()
|
||||||
for _, path := range indexInfo {
|
results, fileTypes := index.Search(query, r.URL.Path, sessionId)
|
||||||
|
for _, path := range results {
|
||||||
responseObj := map[string]interface{}{
|
responseObj := map[string]interface{}{
|
||||||
"path": path,
|
"path": path,
|
||||||
"dir": true,
|
"dir": true,
|
||||||
|
|
|
@ -129,7 +129,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
var token string
|
var token string
|
||||||
if len(hash) > 0 {
|
if len(hash) > 0 {
|
||||||
tokenBuffer := make([]byte, 96) //nolint:gomnd
|
tokenBuffer := make([]byte, 24) //nolint:gomnd
|
||||||
if _, err := rand.Read(tokenBuffer); err != nil {
|
if _, err := rand.Read(tokenBuffer); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,6 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
|
||||||
"LoginPage": auther.LoginPage(),
|
"LoginPage": auther.LoginPage(),
|
||||||
"CSS": false,
|
"CSS": false,
|
||||||
"ReCaptcha": false,
|
"ReCaptcha": false,
|
||||||
"Theme": d.settings.Frontend.Theme,
|
|
||||||
"EnableThumbs": d.server.EnableThumbnails,
|
"EnableThumbs": d.server.EnableThumbnails,
|
||||||
"ResizePreview": d.server.ResizePreview,
|
"ResizePreview": d.server.ResizePreview,
|
||||||
"EnableExec": d.server.EnableExec,
|
"EnableExec": d.server.EnableExec,
|
||||||
|
|
|
@ -124,11 +124,6 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
||||||
return http.StatusBadRequest, errors.ErrEmptyPassword
|
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)
|
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
|
||||||
|
@ -184,7 +179,6 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||||
if !d.user.Perm.Admin && d.user.LockPassword {
|
if !d.user.Perm.Admin && d.user.LockPassword {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package search
|
package index
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -120,3 +121,35 @@ func updateSize(given string) int {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsMatchingType(extension string, matchType string) bool {
|
||||||
|
mimetype := mime.TypeByExtension(extension)
|
||||||
|
if strings.HasPrefix(mimetype, matchType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch matchType {
|
||||||
|
case "doc":
|
||||||
|
return isDoc(extension)
|
||||||
|
case "archive":
|
||||||
|
return isArchive(extension)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDoc(extension string) bool {
|
||||||
|
for _, typefile := range documentTypes {
|
||||||
|
if extension == typefile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isArchive(extension string) bool {
|
||||||
|
for _, typefile := range compressedFile {
|
||||||
|
if extension == typefile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gtsteffaniak/filebrowser/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxIndexSize = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
type Index struct {
|
||||||
|
Dirs []string
|
||||||
|
Files []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootPath string = "/srv"
|
||||||
|
indexes Index
|
||||||
|
indexMutex sync.RWMutex
|
||||||
|
lastIndexed time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetIndex() *Index {
|
||||||
|
return &indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
func Initialize(intervalMinutes uint32) {
|
||||||
|
// Initialize the index
|
||||||
|
indexes = Index{
|
||||||
|
Dirs: make([]string, 0, maxIndexSize),
|
||||||
|
Files: make([]string, 0, maxIndexSize),
|
||||||
|
}
|
||||||
|
rootPath = settings.GlobalConfiguration.Server.Root
|
||||||
|
var numFiles, numDirs int
|
||||||
|
log.Println("Indexing files...")
|
||||||
|
lastIndexedStart := time.Now()
|
||||||
|
// Call the function to index files and directories
|
||||||
|
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
lastIndexed = lastIndexedStart
|
||||||
|
go indexingScheduler(intervalMinutes)
|
||||||
|
log.Println("Successfully indexed files.")
|
||||||
|
log.Println("Files found :", totalNumFiles)
|
||||||
|
log.Println("Directories found :", totalNumDirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexingScheduler(intervalMinutes uint32) {
|
||||||
|
log.Printf("Indexing scheduler will run every %v minutes", intervalMinutes)
|
||||||
|
for {
|
||||||
|
indexes.Dirs = slices.Compact(indexes.Dirs)
|
||||||
|
indexes.Files = slices.Compact(indexes.Files)
|
||||||
|
time.Sleep(time.Duration(intervalMinutes) * time.Minute)
|
||||||
|
var numFiles, numDirs int
|
||||||
|
lastIndexedStart := time.Now()
|
||||||
|
totalNumFiles, totalNumDirs, err := indexFiles(rootPath, &numFiles, &numDirs)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
lastIndexed = lastIndexedStart
|
||||||
|
if totalNumFiles+totalNumDirs > 0 {
|
||||||
|
log.Println("re-indexing found changes and updated the index.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromSlice(slice []string, target string) []string {
|
||||||
|
for i, s := range slice {
|
||||||
|
if s == target {
|
||||||
|
// Swap the target element with the last element
|
||||||
|
slice[i], slice[len(slice)-1] = slice[len(slice)-1], slice[i]
|
||||||
|
// Resize the slice to exclude the last element
|
||||||
|
slice = slice[:len(slice)-1]
|
||||||
|
break // Exit the loop, assuming there's only one target element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a function to recursively index files and directories
|
||||||
|
func indexFiles(path string, numFiles *int, numDirs *int) (int, int, error) {
|
||||||
|
// Check if the current directory has been modified since last indexing
|
||||||
|
dir, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
// directory must have been deleted, remove from index
|
||||||
|
indexes.Dirs = removeFromSlice(indexes.Dirs, path)
|
||||||
|
indexes.Files = removeFromSlice(indexes.Files, path)
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
dirInfo, err := dir.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return *numFiles, *numDirs, err
|
||||||
|
}
|
||||||
|
// Compare the last modified time of the directory with the last indexed time
|
||||||
|
if dirInfo.ModTime().Before(lastIndexed) {
|
||||||
|
return *numFiles, *numDirs, nil
|
||||||
|
}
|
||||||
|
// Read the directory contents
|
||||||
|
files, err := dir.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return *numFiles, *numDirs, err
|
||||||
|
}
|
||||||
|
// Iterate over the files and directories
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
*numDirs++
|
||||||
|
addToIndex(path, file.Name(), true)
|
||||||
|
_, _, err := indexFiles(path+"/"+file.Name(), numFiles, numDirs) // recursive
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Could not index :", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*numFiles++
|
||||||
|
addToIndex(path, file.Name(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return *numFiles, *numDirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToIndex(path string, fileName string, isDir bool) {
|
||||||
|
indexMutex.Lock()
|
||||||
|
defer indexMutex.Unlock()
|
||||||
|
path = strings.TrimPrefix(path, rootPath+"/")
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
adjustedPath := path + "/" + fileName
|
||||||
|
if path == rootPath {
|
||||||
|
adjustedPath = fileName
|
||||||
|
}
|
||||||
|
if isDir {
|
||||||
|
indexes.Dirs = append(indexes.Dirs, adjustedPath)
|
||||||
|
} else {
|
||||||
|
indexes.Files = append(indexes.Files, adjustedPath)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkFillIndex(b *testing.B) {
|
||||||
|
indexes = Index{
|
||||||
|
Dirs: make([]string, 0, 1000),
|
||||||
|
Files: make([]string, 0, 1000),
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMockData(numDirs, numFilesPerDir int) {
|
||||||
|
for i := 0; i < numDirs; i++ {
|
||||||
|
dirName := generateRandomPath(rand.Intn(3) + 1)
|
||||||
|
addToIndex("/", dirName, true)
|
||||||
|
for j := 0; j < numFilesPerDir; j++ {
|
||||||
|
fileName := "file-" + getRandomTerm() + getRandomExtension()
|
||||||
|
addToIndex("/"+dirName, fileName, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomPath(levels int) string {
|
||||||
|
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
dirName := "srv"
|
||||||
|
for i := 0; i < levels; i++ {
|
||||||
|
dirName += "/" + getRandomTerm()
|
||||||
|
}
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomTerm() string {
|
||||||
|
wordbank := []string{
|
||||||
|
"hi", "test", "other", "name",
|
||||||
|
"cool", "things", "more", "items",
|
||||||
|
}
|
||||||
|
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
index := rand.Intn(len(wordbank))
|
||||||
|
return wordbank[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomExtension() string {
|
||||||
|
wordbank := []string{
|
||||||
|
".txt", ".mp3", ".mov", ".doc",
|
||||||
|
".mp4", ".bak", ".zip", ".jpg",
|
||||||
|
}
|
||||||
|
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
index := rand.Intn(len(wordbank))
|
||||||
|
return wordbank[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomSearchTerms(numTerms int) []string {
|
||||||
|
// Generate random search terms
|
||||||
|
searchTerms := make([]string, numTerms)
|
||||||
|
for i := 0; i < numTerms; i++ {
|
||||||
|
searchTerms[i] = getRandomTerm()
|
||||||
|
}
|
||||||
|
return searchTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONBytesEqual compares the JSON in two byte slices.
|
||||||
|
func JSONBytesEqual(a, b []byte) (bool, error) {
|
||||||
|
var j, j2 interface{}
|
||||||
|
if err := json.Unmarshal(a, &j); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &j2); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(j2, j), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIndex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want *map[string][]string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := GetIndex(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("GetIndex() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeIndex(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
intervalMinutes uint32
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
Initialize(tt.args.intervalMinutes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_indexingScheduler(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
intervalMinutes uint32
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
indexingScheduler(tt.args.intervalMinutes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_indexFiles(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
numFiles *int
|
||||||
|
numDirs *int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want int
|
||||||
|
want1 int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1, err := indexFiles(tt.args.path, tt.args.numFiles, tt.args.numDirs)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("indexFiles() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("indexFiles() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
if got1 != tt.want1 {
|
||||||
|
t.Errorf("indexFiles() got1 = %v, want %v", got1, tt.want1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_addToIndex(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
fileName string
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addToIndex(tt.args.path, tt.args.fileName, tt.args.isDir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sessionInProgress sync.Map
|
||||||
|
mutex sync.RWMutex
|
||||||
|
maxSearchResults = 100
|
||||||
|
bytesInMegabyte int64 = 1000000
|
||||||
|
)
|
||||||
|
|
||||||
|
func (si *Index) Search(search string, scope string, sourceSession string) ([]string, map[string]map[string]bool) {
|
||||||
|
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
|
||||||
|
for _, searchTerm := range searchOptions.Terms {
|
||||||
|
if searchTerm == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Iterate over the embedded index.Index fields Dirs and Files
|
||||||
|
for _, i := range []string{"Dirs", "Files"} {
|
||||||
|
isDir := false
|
||||||
|
count := 0
|
||||||
|
var paths []string
|
||||||
|
|
||||||
|
switch i {
|
||||||
|
case "Dirs":
|
||||||
|
isDir = true
|
||||||
|
paths = si.Dirs
|
||||||
|
case "Files":
|
||||||
|
paths = si.Files
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
value, found := sessionInProgress.Load(sourceSession)
|
||||||
|
if !found || value != runningHash {
|
||||||
|
return []string{}, map[string]map[string]bool{}
|
||||||
|
}
|
||||||
|
if count > maxSearchResults {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pathName := scopedPathNameFilter(path, scope)
|
||||||
|
if pathName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matches, fileType := containsSearchTerm(path, searchTerm, *searchOptions, isDir)
|
||||||
|
if !matches {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isDir {
|
||||||
|
fileListTypes[pathName+"/"] = fileType
|
||||||
|
} else {
|
||||||
|
fileListTypes[pathName] = fileType
|
||||||
|
}
|
||||||
|
matching = append(matching, pathName)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort the strings based on the number of elements after splitting by "/"
|
||||||
|
sort.Slice(matching, func(i, j int) bool {
|
||||||
|
parts1 := strings.Split(matching[i], "/")
|
||||||
|
parts2 := strings.Split(matching[j], "/")
|
||||||
|
return len(parts1) < len(parts2)
|
||||||
|
})
|
||||||
|
return matching, fileListTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopedPathNameFilter(pathName string, scope string) string {
|
||||||
|
scope = strings.TrimPrefix(scope, "/")
|
||||||
|
if strings.HasPrefix(pathName, scope) {
|
||||||
|
pathName = strings.TrimPrefix(pathName, scope)
|
||||||
|
} else {
|
||||||
|
pathName = ""
|
||||||
|
}
|
||||||
|
return pathName
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileTypes = map[string]bool{
|
||||||
|
"audio": false,
|
||||||
|
"image": false,
|
||||||
|
"video": false,
|
||||||
|
"doc": false,
|
||||||
|
"archive": false,
|
||||||
|
"dir": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSearchTerm(pathName string, searchTerm string, options SearchOptions, isDir 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 fileTypes {
|
||||||
|
fileTypes[k] = IsMatchingType(extension, k)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkSearchAllIndexes(b *testing.B) {
|
||||||
|
indexes = Index{
|
||||||
|
Dirs: make([]string, 0, 1000),
|
||||||
|
Files: make([]string, 0, 1000),
|
||||||
|
}
|
||||||
|
// Create mock data
|
||||||
|
createMockData(50, 3) // 1000 dirs, 3 files per dir
|
||||||
|
|
||||||
|
// Generate 100 random search terms
|
||||||
|
searchTerms := generateRandomSearchTerms(100)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
// Execute the SearchAllIndexes function
|
||||||
|
for _, term := range searchTerms {
|
||||||
|
indexes.Search(term, "/", "test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop over test files and compare output
|
||||||
|
func TestParseSearch(t *testing.T) {
|
||||||
|
value := ParseSearch("my test search")
|
||||||
|
want := &SearchOptions{
|
||||||
|
Conditions: map[string]bool{
|
||||||
|
"exact": false,
|
||||||
|
},
|
||||||
|
Terms: []string{"my test search"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(value, want) {
|
||||||
|
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||||
|
}
|
||||||
|
value = ParseSearch("case:exact my|test|search")
|
||||||
|
want = &SearchOptions{
|
||||||
|
Conditions: map[string]bool{
|
||||||
|
"exact": true,
|
||||||
|
},
|
||||||
|
Terms: []string{"my", "test", "search"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(value, want) {
|
||||||
|
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||||
|
}
|
||||||
|
value = ParseSearch("type:largerThan=100 type:smallerThan=1000 test")
|
||||||
|
want = &SearchOptions{
|
||||||
|
Conditions: map[string]bool{
|
||||||
|
"exact": false,
|
||||||
|
"larger": true,
|
||||||
|
},
|
||||||
|
Terms: []string{"test"},
|
||||||
|
LargerThan: 100,
|
||||||
|
SmallerThan: 1000,
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(value, want) {
|
||||||
|
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||||
|
}
|
||||||
|
value = ParseSearch("type:audio thisfile")
|
||||||
|
want = &SearchOptions{
|
||||||
|
Conditions: map[string]bool{
|
||||||
|
"exact": false,
|
||||||
|
"audio": true,
|
||||||
|
},
|
||||||
|
Terms: []string{"thisfile"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(value, want) {
|
||||||
|
t.Fatalf("\n got: %+v\n want: %+v", value, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchIndexes(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
search string
|
||||||
|
scope string
|
||||||
|
sourceSession string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []string
|
||||||
|
want1 map[string]map[string]bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1 := indexes.Search(tt.args.search, tt.args.scope, tt.args.sourceSession)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("SearchAllIndexes() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got1, tt.want1) {
|
||||||
|
t.Errorf("SearchAllIndexes() got1 = %v, want %v", got1, tt.want1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_scopedPathNameFilter(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
pathName string
|
||||||
|
scope 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 := scopedPathNameFilter(tt.args.pathName, tt.args.scope); got != tt.want {
|
||||||
|
t.Errorf("scopedPathNameFilter() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_containsSearchTerm(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
pathName string
|
||||||
|
searchTerm string
|
||||||
|
options SearchOptions
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
want1 map[string]bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1 := containsSearchTerm(tt.args.pathName, tt.args.searchTerm, tt.args.options, tt.args.isDir)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("containsSearchTerm() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got1, tt.want1) {
|
||||||
|
t.Errorf("containsSearchTerm() got1 = %v, want %v", got1, tt.want1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isDoc(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
extension string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := isDoc(tt.args.extension); got != tt.want {
|
||||||
|
t.Errorf("isDoc() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getFileSize(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
filepath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want int64
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := getFileSize(tt.args.filepath); got != tt.want {
|
||||||
|
t.Errorf("getFileSize() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isArchive(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
extension string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := isArchive(tt.args.extension); got != tt.want {
|
||||||
|
t.Errorf("isArchive() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getLastPathComponent(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := getLastPathComponent(tt.args.path); got != tt.want {
|
||||||
|
t.Errorf("getLastPathComponent() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_generateRandomHash(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := generateRandomHash(tt.args.length); got != tt.want {
|
||||||
|
t.Errorf("generateRandomHash() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,279 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
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])
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ func Initialize(configFile string) {
|
||||||
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||||
}
|
}
|
||||||
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
|
GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
|
||||||
GlobalConfiguration.Server.Root = "/srv" // hardcoded for now. TODO allow changing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFile(configFile string) []byte {
|
func loadConfigFile(configFile string) []byte {
|
||||||
|
@ -26,7 +25,7 @@ func loadConfigFile(configFile string) []byte {
|
||||||
yamlFile, err := os.Open(configFile)
|
yamlFile, err := os.Open(configFile)
|
||||||
if err != nil {
|
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)
|
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)
|
||||||
setDefaults()
|
GlobalConfiguration = setDefaults()
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
defer yamlFile.Close()
|
defer yamlFile.Close()
|
||||||
|
@ -62,14 +61,18 @@ func setDefaults() Settings {
|
||||||
},
|
},
|
||||||
Auth: Auth{
|
Auth: Auth{
|
||||||
Method: "password",
|
Method: "password",
|
||||||
|
Signup: true,
|
||||||
Recaptcha: Recaptcha{
|
Recaptcha: Recaptcha{
|
||||||
Host: "",
|
Host: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UserDefaults: UserDefaults{
|
UserDefaults: UserDefaults{
|
||||||
Scope: ".",
|
Scope: ".",
|
||||||
LockPassword: false,
|
LockPassword: false,
|
||||||
HideDotfiles: true,
|
HideDotfiles: true,
|
||||||
|
DarkMode: false,
|
||||||
|
DisableSettings: false,
|
||||||
|
Locale: "en",
|
||||||
Permissions: users.Permissions{
|
Permissions: users.Permissions{
|
||||||
Create: true,
|
Create: true,
|
||||||
Rename: true,
|
Rename: true,
|
||||||
|
@ -77,7 +80,23 @@ func setDefaults() Settings {
|
||||||
Delete: true,
|
Delete: true,
|
||||||
Share: true,
|
Share: true,
|
||||||
Download: true,
|
Download: true,
|
||||||
|
Admin: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigLoadChanged(t *testing.T) {
|
||||||
|
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||||
|
// Marshal the YAML data to a more human-readable format
|
||||||
|
newConfig := 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, GlobalConfiguration); diff == "" {
|
||||||
|
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoadSpecificValues(t *testing.T) {
|
||||||
|
yamlData := loadConfigFile("./testingConfig.yaml")
|
||||||
|
// Marshal the YAML data to a more human-readable format
|
||||||
|
newConfig := setDefaults()
|
||||||
|
GlobalConfiguration := setDefaults()
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error unmarshaling YAML data: %v", err)
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
fieldName string
|
||||||
|
globalVal interface{}
|
||||||
|
newVal interface{}
|
||||||
|
}{
|
||||||
|
{"Auth.Method", GlobalConfiguration.Auth.Method, newConfig.Auth.Method},
|
||||||
|
{"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\tGlobalConfig.%s: %v \n\tSetConfig: %v \n", tc.fieldName, tc.globalVal, tc.newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSettings_MakeUserDir(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Key []byte
|
||||||
|
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{
|
||||||
|
Key: tt.fields.Key,
|
||||||
|
Signup: tt.fields.Signup,
|
||||||
|
CreateUserDir: tt.fields.CreateUserDir,
|
||||||
|
UserHomeBasePath: tt.fields.UserHomeBasePath,
|
||||||
|
Commands: tt.fields.Commands,
|
||||||
|
Shell: tt.fields.Shell,
|
||||||
|
AdminUsername: tt.fields.AdminUsername,
|
||||||
|
AdminPassword: tt.fields.AdminPassword,
|
||||||
|
Rules: tt.fields.Rules,
|
||||||
|
Server: tt.fields.Server,
|
||||||
|
Auth: tt.fields.Auth,
|
||||||
|
Frontend: tt.fields.Frontend,
|
||||||
|
Users: tt.fields.Users,
|
||||||
|
UserDefaults: tt.fields.UserDefaults,
|
||||||
|
}
|
||||||
|
got, err := s.MakeUserDir(tt.args.username, tt.args.userScope, tt.args.serverRoot)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Settings.MakeUserDir() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Settings.MakeUserDir() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cleanUsername(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := cleanUsername(tt.args.s); got != tt.want {
|
||||||
|
t.Errorf("cleanUsername() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package settings
|
||||||
import (
|
import (
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
"github.com/gtsteffaniak/filebrowser/errors"
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageBackend is a settings storage backend.
|
// StorageBackend is a settings storage backend.
|
||||||
|
@ -59,7 +58,7 @@ func (s *Storage) Save(set *Settings) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.UserDefaults.ViewMode == "" {
|
if set.UserDefaults.ViewMode == "" {
|
||||||
set.UserDefaults.ViewMode = users.MosaicViewMode
|
set.UserDefaults.ViewMode = "normal"
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.Rules == nil {
|
if set.Rules == nil {
|
||||||
|
|
|
@ -5,19 +5,6 @@ import (
|
||||||
"github.com/gtsteffaniak/filebrowser/users"
|
"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 {
|
type Settings struct {
|
||||||
Key []byte `json:"key"`
|
Key []byte `json:"key"`
|
||||||
Signup bool `json:"signup"`
|
Signup bool `json:"signup"`
|
||||||
|
@ -74,13 +61,13 @@ type Frontend struct {
|
||||||
DisableExternal bool `json:"disableExternal"`
|
DisableExternal bool `json:"disableExternal"`
|
||||||
DisableUsedPercentage bool `json:"disableUsedPercentage"`
|
DisableUsedPercentage bool `json:"disableUsedPercentage"`
|
||||||
Files string `json:"files"`
|
Files string `json:"files"`
|
||||||
Theme string `json:"theme"`
|
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDefaults is a type that holds the default values
|
// UserDefaults is a type that holds the default values
|
||||||
// for some fields on User.
|
// for some fields on User.
|
||||||
type UserDefaults struct {
|
type UserDefaults struct {
|
||||||
|
<<<<<<< HEAD
|
||||||
LockPassword bool `json:"lockPassword"`
|
LockPassword bool `json:"lockPassword"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
|
@ -88,12 +75,27 @@ type UserDefaults struct {
|
||||||
SingleClick bool `json:"singleClick"`
|
SingleClick bool `json:"singleClick"`
|
||||||
Rules []rules.Rule `json:"rules"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
Sorting struct {
|
Sorting 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 {
|
||||||
|
>>>>>>> v0.2.1
|
||||||
By string `json:"by"`
|
By string `json:"by"`
|
||||||
Asc bool `json:"asc"`
|
Asc bool `json:"asc"`
|
||||||
} `json:"sorting"`
|
} `json:"sorting"`
|
||||||
Perm users.Permissions `json:"perm"`
|
Perm users.Permissions `json:"perm"`
|
||||||
Permissions users.Permissions `json:"permissions"`
|
Permissions users.Permissions `json:"permissions"`
|
||||||
|
<<<<<<< HEAD
|
||||||
Commands []string `json:"commands,omitemptys"`
|
Commands []string `json:"commands,omitemptys"`
|
||||||
|
=======
|
||||||
|
Commands []string `json:"commands,omitempty"`
|
||||||
|
>>>>>>> v0.2.1
|
||||||
HideDotfiles bool `json:"hideDotfiles"`
|
HideDotfiles bool `json:"hideDotfiles"`
|
||||||
DateFormat bool `json:"dateFormat"`
|
DateFormat bool `json:"dateFormat"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,13 @@ frontend:
|
||||||
disableExternal: true
|
disableExternal: true
|
||||||
disableUsedPercentage: true
|
disableUsedPercentage: true
|
||||||
files: ""
|
files: ""
|
||||||
theme: ""
|
|
||||||
color: ""
|
color: ""
|
||||||
userDefaults:
|
userDefaults:
|
||||||
scope: ""
|
scope: ""
|
||||||
locale: ""
|
locale: ""
|
||||||
viewMode: ""
|
viewMode: ""
|
||||||
|
darkMode: true
|
||||||
|
disableSettings: false
|
||||||
singleClick: true
|
singleClick: true
|
||||||
sorting:
|
sorting:
|
||||||
by: ""
|
by: ""
|
||||||
|
|
|
@ -2,6 +2,7 @@ package bolt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
|
@ -73,11 +74,20 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st usersBackend) Save(user *users.User) error {
|
func (st usersBackend) Save(user *users.User) error {
|
||||||
|
<<<<<<< HEAD
|
||||||
password, err := users.HashPwd(user.Password)
|
password, err := users.HashPwd(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
user.Password = password
|
user.Password = password
|
||||||
|
=======
|
||||||
|
log.Println("userinfo", user.Password)
|
||||||
|
pass, err := users.HashPwd(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Password = pass
|
||||||
|
>>>>>>> v0.2.1
|
||||||
err = st.db.Save(user)
|
err = st.db.Save(user)
|
||||||
if err == storm.ErrAlreadyExists {
|
if err == storm.ErrAlreadyExists {
|
||||||
return errors.ErrExist
|
return errors.ErrExist
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HashPwd hashes a password.
|
// HashPwd hashes a password.
|
||||||
func HashPwd(password string) (string, error) {
|
func HashPwd(password string) (string, error) {
|
||||||
|
log.Println("hashing password", password)
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
return string(bytes), err
|
return string(bytes), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||||
|
|
||||||
// Update updates a user in the database.
|
// Update updates a user in the database.
|
||||||
func (s *Storage) Update(user *User, fields ...string) error {
|
func (s *Storage) Update(user *User, fields ...string) error {
|
||||||
err := user.Clean("", fields...)
|
err := user.Clean("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,10 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/gtsteffaniak/filebrowser/errors"
|
|
||||||
"github.com/gtsteffaniak/filebrowser/files"
|
"github.com/gtsteffaniak/filebrowser/files"
|
||||||
"github.com/gtsteffaniak/filebrowser/rules"
|
"github.com/gtsteffaniak/filebrowser/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ListViewMode = "list"
|
|
||||||
MosaicViewMode = "mosaic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Permissions struct {
|
type Permissions struct {
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
Execute bool `json:"execute"`
|
Execute bool `json:"execute"`
|
||||||
|
@ -29,21 +23,23 @@ type Permissions struct {
|
||||||
|
|
||||||
// User describes a user.
|
// User describes a user.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `storm:"id,increment" json:"id"`
|
DarkMode bool `json:"darkMode"`
|
||||||
Username string `storm:"unique" json:"username"`
|
DisableSettings bool `json:"disableSettings"`
|
||||||
Password string `json:"password"`
|
ID uint `storm:"id,increment" json:"id"`
|
||||||
Scope string `json:"scope"`
|
Username string `storm:"unique" json:"username"`
|
||||||
Locale string `json:"locale"`
|
Password string `json:"password"`
|
||||||
LockPassword bool `json:"lockPassword"`
|
Scope string `json:"scope"`
|
||||||
ViewMode string `json:"viewMode"`
|
Locale string `json:"locale"`
|
||||||
SingleClick bool `json:"singleClick"`
|
LockPassword bool `json:"lockPassword"`
|
||||||
Perm Permissions `json:"perm"`
|
ViewMode string `json:"viewMode"`
|
||||||
Commands []string `json:"commands"`
|
SingleClick bool `json:"singleClick"`
|
||||||
Sorting files.Sorting `json:"sorting"`
|
Perm Permissions `json:"perm"`
|
||||||
Fs afero.Fs `json:"-" yaml:"-"`
|
Commands []string `json:"commands"`
|
||||||
Rules []rules.Rule `json:"rules"`
|
Sorting files.Sorting `json:"sorting"`
|
||||||
HideDotfiles bool `json:"hideDotfiles"`
|
Fs afero.Fs `json:"-" yaml:"-"`
|
||||||
DateFormat bool `json:"dateFormat"`
|
Rules []rules.Rule `json:"rules"`
|
||||||
|
HideDotfiles bool `json:"hideDotfiles"`
|
||||||
|
DateFormat bool `json:"dateFormat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRules implements rules.Provider.
|
// GetRules implements rules.Provider.
|
||||||
|
@ -51,53 +47,11 @@ func (u *User) GetRules() []rules.Rule {
|
||||||
return u.Rules
|
return u.Rules
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkableFields = []string{
|
|
||||||
"Username",
|
|
||||||
"Password",
|
|
||||||
"Scope",
|
|
||||||
"ViewMode",
|
|
||||||
"Commands",
|
|
||||||
"Sorting",
|
|
||||||
"Rules",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean cleans up a user and verifies if all its fields
|
// Clean cleans up a user and verifies if all its fields
|
||||||
// are alright to be saved.
|
// are alright to be saved.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (u *User) Clean(baseScope string, fields ...string) error {
|
func (u *User) Clean(baseScope 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 {
|
if u.Fs == nil {
|
||||||
scope := u.Scope
|
scope := u.Scope
|
||||||
|
|
|
@ -2,7 +2,7 @@ package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Version is the current File Browser version.
|
// Version is the current File Browser version.
|
||||||
Version = "(0.2.0)"
|
Version = "(0.2.1)"
|
||||||
// CommitSHA is the commmit sha.
|
// CommitSHA is the commmit sha.
|
||||||
CommitSHA = "(unknown)"
|
CommitSHA = "(unknown)"
|
||||||
)
|
)
|
||||||
|
|
131
configuration.md
131
configuration.md
|
@ -7,6 +7,9 @@ This document covers the available configuration options, their defaults, and ho
|
||||||
Here is an expanded config file which includes all possible configurations:
|
Here is an expanded config file which includes all possible configurations:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
signup: false
|
||||||
|
adminUsername: admin
|
||||||
|
adminPassword: admin
|
||||||
server:
|
server:
|
||||||
indexingInterval: 5
|
indexingInterval: 5
|
||||||
numImageProcessors: 4
|
numImageProcessors: 4
|
||||||
|
@ -30,18 +33,18 @@ auth:
|
||||||
header: ""
|
header: ""
|
||||||
method: json
|
method: json
|
||||||
command: ""
|
command: ""
|
||||||
signup: false
|
|
||||||
shell: ""
|
shell: ""
|
||||||
frontend:
|
frontend:
|
||||||
name: ""
|
name: ""
|
||||||
disableExternal: false
|
disableExternal: false
|
||||||
disableUsedPercentage: true
|
disableUsedPercentage: true
|
||||||
files: ""
|
files: ""
|
||||||
theme: ""
|
|
||||||
color: ""
|
color: ""
|
||||||
userDefaults:
|
userDefaults:
|
||||||
|
settingsAllowed: true
|
||||||
|
darkMode: false
|
||||||
scope: ""
|
scope: ""
|
||||||
locale: ""
|
locale: "en"
|
||||||
viewMode: ""
|
viewMode: ""
|
||||||
singleClick: true
|
singleClick: true
|
||||||
sorting:
|
sorting:
|
||||||
|
@ -64,34 +67,38 @@ userDefaults:
|
||||||
Here are the defaults if nothing is set:
|
Here are the defaults if nothing is set:
|
||||||
|
|
||||||
```
|
```
|
||||||
Signup: true
|
signup: true
|
||||||
AdminUsername: admin
|
adminUsername: admin
|
||||||
AdminPassword: admin
|
adminPassword: admin
|
||||||
Server:
|
server:
|
||||||
EnableThumbnails: true
|
enableThumbnails: true
|
||||||
EnableExec: false
|
enableExec: false
|
||||||
IndexingInterval: 5
|
indexingInterval: 5
|
||||||
Port: 8080
|
port: 8080
|
||||||
NumImageProcessors: 4
|
numImageProcessors: 4
|
||||||
BaseURL: ""
|
baseURL: ""
|
||||||
Database: database.db
|
database: database.db
|
||||||
Log: stdout
|
log: stdout
|
||||||
Root: /srv
|
root: /srv
|
||||||
Auth:
|
auth:
|
||||||
Method: password
|
method: password
|
||||||
Recaptcha:
|
recaptcha:
|
||||||
Host: ""
|
host: ""
|
||||||
UserDefaults:
|
userDefaults:
|
||||||
Scope: "."
|
settingsAllowed: true
|
||||||
LockPassword: false
|
darkMode: false
|
||||||
HideDotfiles: true
|
scope: ""
|
||||||
Permissions:
|
locale: "en"
|
||||||
Create: true
|
scope: "."
|
||||||
Rename: true
|
lockPassword: false
|
||||||
Modify: true
|
hideDotfiles: true
|
||||||
Delete: true
|
permissions:
|
||||||
Share: true
|
create: true
|
||||||
Download: true
|
rename: true
|
||||||
|
modify: true
|
||||||
|
delete: true
|
||||||
|
share: true
|
||||||
|
download: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## About each configuration
|
## About each configuration
|
||||||
|
@ -100,17 +107,17 @@ UserDefaults:
|
||||||
|
|
||||||
## About each configuration
|
## About each configuration
|
||||||
|
|
||||||
- `Signup`: This boolean value indicates whether user signup is enabled.
|
- `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.
|
- `AdminUsername`: This is the username of the admin user. Default: `admin`
|
||||||
|
|
||||||
- `AdminPassword`: This is the password of the admin user.
|
- `AdminPassword`: This is the password of the admin user. Default: `admin`
|
||||||
|
|
||||||
### Server configuration settings
|
### Server configuration settings
|
||||||
|
|
||||||
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes (used in search only).
|
- `indexingInterval`: This is the time in minutes the system waits before checking for filesystem changes. Default: `5`
|
||||||
|
|
||||||
- `numImageProcessors`: This is the number of image processors available.
|
- `numImageProcessors`: This is the number of image processors available. Default: `4`
|
||||||
|
|
||||||
- `socket`: This is the socket configuration.
|
- `socket`: This is the socket configuration.
|
||||||
|
|
||||||
|
@ -118,23 +125,23 @@ UserDefaults:
|
||||||
|
|
||||||
- `tlsCert`: This is the TLS certificate configuration.
|
- `tlsCert`: This is the TLS certificate configuration.
|
||||||
|
|
||||||
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled.
|
- `enableThumbnails`: This boolean value determines whether thumbnails are enabled on ui. Default: `true`
|
||||||
|
|
||||||
- `resizePreview`: This boolean value determines whether preview resizing is enabled.
|
- `resizePreview`: This boolean value determines whether preview resizing is enabled. Default: `false`
|
||||||
|
|
||||||
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
|
- `typeDetectionByHeader`: This boolean value determines whether type detection is based on headers.
|
||||||
|
|
||||||
- `port`: This is the port number on which the server is running.
|
- `port`: This is the port number on which the server is running. Default: `8080`
|
||||||
|
|
||||||
- `baseURL`: This is the base URL for the server.
|
- `baseURL`: This is the base URL for the server. Default: `""`
|
||||||
|
|
||||||
- `address`: This is the server address configuration.
|
- `address`: This is the server address configuration. Default: `0.0.0.0`
|
||||||
|
|
||||||
- `log`: This specifies the log destination (e.g., "stdout" for standard output).
|
- `log`: This specifies the log destination. Default: `stdout`
|
||||||
|
|
||||||
- `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.
|
- `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`
|
||||||
|
|
||||||
- `root`: This is the root directory path.
|
- `root`: This is the root directory path. Default: `/srv`
|
||||||
|
|
||||||
### Auth configuration settings
|
### Auth configuration settings
|
||||||
|
|
||||||
|
@ -149,17 +156,16 @@ UserDefaults:
|
||||||
- `header`: This is the authentication header.
|
- `header`: This is the authentication header.
|
||||||
|
|
||||||
- `method`: This is the authentication method used (e.g., "json"). Possible values:
|
- `method`: This is the authentication method used (e.g., "json"). Possible values:
|
||||||
- password - username and password
|
- `password` - username and password
|
||||||
- hook - hook authentication
|
- `hook` - hook authentication
|
||||||
- proxy - proxy authentication
|
- `proxy` - proxy authentication
|
||||||
- oath - oath authentication
|
- `oath` - oath authentication
|
||||||
|
- `noauth` - no authentication/login required.
|
||||||
|
|
||||||
- `command`: This is the authentication command.
|
- `command`: Deprecated: This is the authentication command.
|
||||||
|
|
||||||
- `signup`: This boolean value indicates whether user signup is enabled.
|
|
||||||
|
|
||||||
- `shell`: This is the shell configuration.
|
- `shell`: This is the shell configuration.
|
||||||
|
|
||||||
### Frontend configuration settings
|
### Frontend configuration settings
|
||||||
|
|
||||||
- `name`: This is the name of the frontend.
|
- `name`: This is the name of the frontend.
|
||||||
|
@ -173,22 +179,26 @@ UserDefaults:
|
||||||
- `theme`: This is the theme configuration.
|
- `theme`: This is the theme configuration.
|
||||||
|
|
||||||
- `color`: This is the color configuration.
|
- `color`: This is the color configuration.
|
||||||
|
|
||||||
### UserDefaults configuration settings
|
### 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.
|
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
|
||||||
|
|
||||||
- `locale`: This is the locale configuration.
|
- `locale`: String locale configuration. Default: `en`
|
||||||
|
|
||||||
- `viewMode`: This is the view mode configuration.
|
- `viewMode`: This is the view mode configuration. Possible values: `normal`, `compact`, `list`, and `gallery`. default: `normal`
|
||||||
|
|
||||||
- `singleClick`: This boolean value determines whether single-click is enabled.
|
- `singleClick`: This boolean value determines whether single-click is enabled. (`true` or `false`)
|
||||||
|
|
||||||
- `sorting`:
|
- `sorting`:
|
||||||
|
|
||||||
- `by`: This is the sorting method used (e.g., "asc").
|
- `by`: This is the sorting method used (e.g., "asc").
|
||||||
|
|
||||||
- `asc`: This boolean value determines the sorting order.
|
- `asc`: This boolean value determines the sorting order is ascending or descending. (`true` or `false`)
|
||||||
|
|
||||||
- `permissions`:
|
- `permissions`:
|
||||||
|
|
||||||
|
@ -208,9 +218,8 @@ UserDefaults:
|
||||||
|
|
||||||
- `download`: This boolean value determines whether download permissions are granted.
|
- `download`: This boolean value determines whether download permissions are granted.
|
||||||
|
|
||||||
- `commands`: This is a list of commands.
|
- `commands`: Deprecated: This is a list of commands.
|
||||||
|
|
||||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden.
|
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
|
||||||
|
|
||||||
- `dateFormat`: This boolean value determines whether date formatting is enabled.
|
|
||||||
|
|
||||||
|
- `dateFormat`: This boolean value determines whether date formatting is enabled. (`true` or `false`)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
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
|
File diff suppressed because it is too large
Load Diff
|
@ -39,6 +39,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"compression-webpack-plugin": "^10.0.0",
|
"compression-webpack-plugin": "^10.0.0",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
|
|
|
@ -128,8 +128,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[{[ if .Theme -]}]
|
[{[ if .darkMode -]}]
|
||||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
|
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/dark.css" />
|
||||||
[{[ end ]}]
|
[{[ end ]}]
|
||||||
[{[ if .CSS -]}]
|
[{[ if .CSS -]}]
|
||||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||||
|
|
|
@ -1,220 +0,0 @@
|
||||||
:root {
|
|
||||||
--background: #141D24;
|
|
||||||
--surfacePrimary: #20292F;
|
|
||||||
--surfaceSecondary: #3A4147;
|
|
||||||
--divider: rgba(255, 255, 255, 0.12);
|
|
||||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
|
||||||
--textSecondary: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
#login {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
|
||||||
header {
|
|
||||||
background: transparent;
|
|
||||||
backdrop-filter: blur(16px) invert(0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#search #input {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
border-color: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
#search #input input::placeholder {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
#search.active #input {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
border-color: white;
|
|
||||||
}
|
|
||||||
#search.active input {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
#search #result {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
#search .boxes h3 {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.action:hover {
|
|
||||||
background-color: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action .counter {
|
|
||||||
border-color: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav > div {
|
|
||||||
border-color: var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
border-color: var(--divider);
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.breadcrumbs span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.breadcrumbs a:hover {
|
|
||||||
background-color: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing .item {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
border-color: var(--divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing .item .modified {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
#listing h2,
|
|
||||||
#listing.list .header span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
#listing.list .header span {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing.list .item.header {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.button--flat:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard #nav ul li {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
.dashboard #nav ul li:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
#result-list {
|
|
||||||
background-color:#292929;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h3,
|
|
||||||
.dashboard #nav,
|
|
||||||
.dashboard p label {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.card#share input,
|
|
||||||
.card#share select,
|
|
||||||
.input {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:hover,
|
|
||||||
.input:focus {
|
|
||||||
border-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
.input--red {
|
|
||||||
background: #73302D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input--green {
|
|
||||||
background: #147A41;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard #nav .wrapper,
|
|
||||||
.collapsible {
|
|
||||||
border-color: var(--divider);
|
|
||||||
}
|
|
||||||
.collapsible > label * {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
table th {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
.file-list li:before {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.shell__result {
|
|
||||||
border-top: 1px solid var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor-container {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor-container .bar {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#file-selection {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
#file-selection span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
#dropdown {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__box {
|
|
||||||
background: var(--surfacePrimary) !important;
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__box__element {
|
|
||||||
border-top-color: var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.helpButton {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
.sizeInputWrapper {
|
|
||||||
background: var(--background);
|
|
||||||
color: white
|
|
||||||
}
|
|
||||||
.button-group button {
|
|
||||||
background: var(--background);
|
|
||||||
color: white
|
|
||||||
}
|
|
||||||
#result-desktop #result-list {
|
|
||||||
background: #2a3137;
|
|
||||||
max-height: unset;
|
|
||||||
}
|
|
|
@ -1,18 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "app",
|
name: "app",
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const loading = document.getElementById("loading");
|
const loading = document.getElementById("loading");
|
||||||
loading.classList.add("done");
|
loading.classList.add("done");
|
||||||
|
setTimeout(() => {
|
||||||
setTimeout(function () {
|
|
||||||
loading.parentNode.removeChild(loading);
|
loading.parentNode.removeChild(loading);
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
|
@ -20,5 +22,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Always load styles.css */
|
||||||
@import "./css/styles.css";
|
@import "./css/styles.css";
|
||||||
|
@import "./css/dark.css";
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
|
<div id="search" @click="open" v-bind:class="{ active, ongoing, 'dark-mode': isDarkMode }">
|
||||||
<div id="input">
|
<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>
|
<i class="material-icons">close</i>
|
||||||
|
@ -149,6 +149,7 @@
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
-webkit-transition: width 0.3s ease 0s;
|
-webkit-transition: width 0.3s ease 0s;
|
||||||
transition: width 0.3s ease 0s;
|
transition: width 0.3s ease 0s;
|
||||||
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#result-desktop {
|
#result-desktop {
|
||||||
|
@ -172,6 +173,8 @@
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search.active #result-desktop ul li a {
|
#search.active #result-desktop ul li a {
|
||||||
|
@ -201,6 +204,7 @@
|
||||||
|
|
||||||
/* Search */
|
/* Search */
|
||||||
#search {
|
#search {
|
||||||
|
background-color:unset;
|
||||||
z-index:3;
|
z-index:3;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: .5em;
|
top: .5em;
|
||||||
|
@ -314,7 +318,7 @@ body.rtl #search #result ul>* {
|
||||||
}
|
}
|
||||||
|
|
||||||
#search.active #input {
|
#search.active #input {
|
||||||
background-color: lightgray;
|
background-color: var(--background);
|
||||||
border-color: black;
|
border-color: black;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-bottom-style: none;
|
border-bottom-style: none;
|
||||||
|
@ -485,7 +489,7 @@ export default {
|
||||||
{ label: "Photos", value: "type:image" },
|
{ label: "Photos", value: "type:image" },
|
||||||
{ label: "Audio", value: "type:audio" },
|
{ label: "Audio", value: "type:audio" },
|
||||||
{ label: "Videos", value: "type:video" },
|
{ label: "Videos", value: "type:video" },
|
||||||
{ label: "Documents", value: "type:docs" },
|
{ label: "Documents", value: "type:doc" },
|
||||||
{ label: "Archives", value: "type:archive" },
|
{ label: "Archives", value: "type:archive" },
|
||||||
],
|
],
|
||||||
value: "",
|
value: "",
|
||||||
|
@ -538,6 +542,9 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user", "show"]),
|
...mapState(["user", "show"]),
|
||||||
...mapGetters(["isListing"]),
|
...mapGetters(["isListing"]),
|
||||||
|
isDarkMode() {
|
||||||
|
return this.user.darkMode === true
|
||||||
|
},
|
||||||
showBoxes() {
|
showBoxes() {
|
||||||
return this.searchTypes == "";
|
return this.searchTypes == "";
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<nav :class="{ active }">
|
<nav :class="{ active, 'dark-mode': isDarkMode }">
|
||||||
<template v-if="isLogged">
|
<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>
|
<i class="material-icons">folder</i>
|
||||||
|
@ -87,6 +87,9 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user"]),
|
...mapState(["user"]),
|
||||||
|
isDarkMode() {
|
||||||
|
return this.user.darkMode === true
|
||||||
|
},
|
||||||
...mapGetters(["isLogged"]),
|
...mapGetters(["isLogged"]),
|
||||||
active() {
|
active() {
|
||||||
return this.$store.state.show === "sidebar";
|
return this.$store.state.show === "sidebar";
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default {
|
||||||
|
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("resize", this.onResize);
|
window.removeEventListener("resize", this.onResize);
|
||||||
document.removeEventListener("mouseup", this.onMouseUp);
|
document.removeEventListener("mouseup", this.onMouseUp);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,14 +6,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { logoURL } from "@/utils/constants";
|
import { logoURL } from "@/utils/constants";
|
||||||
import Action from "@/components/header/Action.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "header-bar",
|
name: "header-bar",
|
||||||
props: ["showLogo", "showMenu"],
|
props: ["showLogo", "showMenu"],
|
||||||
components: {
|
|
||||||
Action,
|
|
||||||
},
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
logoURL,
|
logoURL,
|
||||||
|
|
|
@ -178,7 +178,7 @@ export default {
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
this.$showSuccess(this.$t("success.linkCopied"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
this.clip.destroy();
|
this.clip.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<select v-on:change="change" :value="viewMode">
|
||||||
|
<option v-for="mode in viewModes" :key="mode" :value="mode">
|
||||||
|
{{ mode }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ViewMode",
|
||||||
|
props: ["viewMode"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
change(event) {
|
||||||
|
this.$emit("update:viewMode", event.target.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -2,7 +2,7 @@
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
border-radius: .1em;
|
border-radius: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--blue);
|
background: var(--blue);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.input {
|
.input {
|
||||||
border-radius: .1em;
|
border-radius: 1em;
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
.share__box {
|
.share__box {
|
||||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 0.2em;
|
border-radius: 1em;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,6 +203,7 @@ body.rtl .breadcrumbs a {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.dark-mode header {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header with backdrop-filter support */
|
||||||
|
@supports (backdrop-filter: none) {
|
||||||
|
.dark-mode header {
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: blur(16px) invert(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#search.dark-mode input {
|
||||||
|
color:white
|
||||||
|
}
|
||||||
|
|
||||||
|
#search.active.dark-mode #input {
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
/* Search input */
|
||||||
|
.dark-mode #search #input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
border-color: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode #search #input input::placeholder {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Search input */
|
||||||
|
.dark-mode #search.active #input {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode #search.active input {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search result */
|
||||||
|
.dark-mode #search #result {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search boxes */
|
||||||
|
.dark-mode #search .boxes h3 {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action */
|
||||||
|
.dark-mode .action {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .action:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action counter */
|
||||||
|
.dark-mode .action .counter {
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.dark-mode nav > div {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs */
|
||||||
|
.dark-mode .breadcrumbs {
|
||||||
|
border-color: var(--divider);
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .breadcrumbs span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .breadcrumbs a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Listing items */
|
||||||
|
.dark-mode #listing .item {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
border-color: var(--divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Listing item modified text */
|
||||||
|
.dark-mode #listing .item .modified {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Listing header and span */
|
||||||
|
.dark-mode #listing h2,
|
||||||
|
.dark-mode #listing.list .header span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message */
|
||||||
|
.dark-mode .message {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.dark-mode .card {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flat button hover */
|
||||||
|
.dark-mode .button--flat:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard navigation */
|
||||||
|
.dark-mode .dashboard #nav ul li {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .dashboard #nav ul li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
#search.active.dark-mode #result {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
/* Result list */
|
||||||
|
.dark-mode #result-list {
|
||||||
|
background-color: var(--surfacePrimary);
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card, Dashboard navigation, and label */
|
||||||
|
.dark-mode .card h3,
|
||||||
|
.dark-mode .dashboard #nav,
|
||||||
|
.dark-mode .dashboard p label {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .card#share input,
|
||||||
|
.dark-mode .card#share select,
|
||||||
|
.dark-mode .input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input hover and focus */
|
||||||
|
.dark-mode .input:hover,
|
||||||
|
.dark-mode .input:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red input */
|
||||||
|
.dark-mode .input--red {
|
||||||
|
background: #73302D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green input */
|
||||||
|
.dark-mode .input--green {
|
||||||
|
background: #147A41;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible and label */
|
||||||
|
.dark-mode .dashboard #nav .wrapper,
|
||||||
|
.dark-mode .collapsible {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .collapsible > label * {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table header */
|
||||||
|
.dark-mode table th {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File list item */
|
||||||
|
.dark-mode .file-list li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .file-list li:before {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shell */
|
||||||
|
.dark-mode .shell {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shell result */
|
||||||
|
.dark-mode .shell__result {
|
||||||
|
border-top: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor container */
|
||||||
|
.dark-mode #editor-container {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode #editor-container .bar {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.dark-mode nav {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File selection */
|
||||||
|
.dark-mode #file-selection {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode #file-selection span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.dark-mode #dropdown {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share box */
|
||||||
|
.dark-mode .share__box {
|
||||||
|
background: var(--surfacePrimary) !important;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share box element */
|
||||||
|
.dark-mode .share__box__element {
|
||||||
|
border-top-color: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help button */
|
||||||
|
.dark-mode .helpButton {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size input wrapper */
|
||||||
|
.dark-mode .sizeInputWrapper {
|
||||||
|
background: var(--background);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button group button */
|
||||||
|
.dark-mode .button-group button {
|
||||||
|
background: var(--background);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result desktop */
|
||||||
|
.dark-mode #result-desktop #result-list {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result desktop background */
|
||||||
|
.dark-mode #result-desktop {
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
|
@ -166,6 +166,7 @@ table tr>*:last-child {
|
||||||
border-radius: 2px;
|
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);
|
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;
|
overflow: auto;
|
||||||
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.floating {
|
.card.floating {
|
||||||
|
@ -459,7 +460,7 @@ body.rtl .card .card-title>*:first-child {
|
||||||
.card .card-action.full .action {
|
.card .card-action.full .action {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
border-radius: 0.2em;
|
border-radius: 1em;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,41 +85,53 @@ body.rtl #listing {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic {
|
#listing {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
margin: 0 -0.5em;
|
margin: 0 -0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic .item {
|
#listing.gallery .item,
|
||||||
|
#listing.compact .item,
|
||||||
|
#listing.normal .item,
|
||||||
|
#listing.list .item {
|
||||||
width: calc(33% - 1em);
|
width: calc(33% - 1em);
|
||||||
|
max-width: 300px;
|
||||||
margin: .5em;
|
margin: .5em;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.2em;
|
border-radius: 1em;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
|
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.mosaic .item:hover {
|
#listing .item:hover {
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic .header {
|
#listing .header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic .item div:first-of-type {
|
#listing .item div:first-of-type {
|
||||||
width: 5em;
|
width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic .item div:last-of-type {
|
#listing .item div:last-of-type {
|
||||||
width: calc(100% - 5vw);
|
width: calc(100% - 5vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic.gallery .item div:first-of-type {
|
#listing.gallery .item div:first-of-type {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 12em;
|
height: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic.gallery .item div:last-of-type {
|
#listing.gallery .item div:last-of-type {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.5em;
|
bottom: 0.5em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
@ -127,19 +139,19 @@ body.rtl #listing {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic.gallery .item[data-type=image] div:last-of-type {
|
#listing.gallery .item[data-type=image] div:last-of-type {
|
||||||
color: white;
|
color: white;
|
||||||
background: linear-gradient(#0000, #0009);
|
background: linear-gradient(#0000, #0009);
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic.gallery .item i {
|
#listing.gallery .item i {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
font-size: 8em;
|
font-size: 8em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.mosaic.gallery .item img {
|
#listing.gallery .item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -149,6 +161,109 @@ body.rtl #listing {
|
||||||
display: none;
|
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 {
|
#listing.list {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -160,14 +275,10 @@ body.rtl #listing {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
padding: 0;
|
padding: .5em;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list h2 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing .item[aria-selected=true] {
|
#listing .item[aria-selected=true] {
|
||||||
background: var(--blue) !important;
|
background: var(--blue) !important;
|
||||||
color: var(--item-selected) !important;
|
color: var(--item-selected) !important;
|
||||||
|
@ -200,7 +311,7 @@ body.rtl #listing {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing .item.header {
|
#listing .header {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
@ -211,20 +322,34 @@ body.rtl #listing {
|
||||||
margin-left: .2em;
|
margin-left: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header {
|
#listing.compact .header,
|
||||||
|
#listing.list .header {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
background: #fafafa;
|
background: var(--surfacePrimary);
|
||||||
|
border-top-left-radius: 1em;
|
||||||
|
border-top-right-radius: 1em;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
padding: .85em;
|
padding: .85em;
|
||||||
|
width:100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
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.header>div:first-child {
|
#listing.list .item:last-child {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
border-bottom-left-radius: 1em;
|
||||||
|
border-bottom-right-radius: 1em;
|
||||||
|
}
|
||||||
|
#listing.list .header>div:first-child {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header .name {
|
#listing.list .header .name {
|
||||||
margin-right: 3em;
|
margin-right: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +357,7 @@ body.rtl #listing {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header>div:first-child {
|
#listing.list .header>div:first-child {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +365,7 @@ body.rtl #listing {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header .name {
|
#listing.list .header .name {
|
||||||
margin-right: 3em;
|
margin-right: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,7 +383,7 @@ body.rtl #listing {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header .active {
|
#listing.list .header .active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
|
#listing.list .item div:last-of-type{
|
||||||
|
display:block;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
padding-bottom: 5em;
|
padding-bottom: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item .size {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing.list .item .name {
|
#listing.list .item .name {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,9 @@
|
||||||
#listing {
|
#listing {
|
||||||
margin-bottom: 5em;
|
margin-bottom: 5em;
|
||||||
}
|
}
|
||||||
|
#listing .item {
|
||||||
|
min-width: 100%
|
||||||
|
}
|
||||||
body.rtl #listing {
|
body.rtl #listing {
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
|
@ -117,9 +119,6 @@
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 450px) {
|
@media (max-width: 450px) {
|
||||||
#listing.list .item .modified {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing.list .item .name {
|
#listing.list .item .name {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -11,9 +11,7 @@ export function parseToken(token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(Base64.decode(parts[1]));
|
const data = JSON.parse(Base64.decode(parts[1]));
|
||||||
|
|
||||||
document.cookie = `auth=${token}; path=/`;
|
document.cookie = `auth=${token}; path=/`;
|
||||||
|
|
||||||
localStorage.setItem("jwt", token);
|
localStorage.setItem("jwt", token);
|
||||||
store.commit("setJWT", token);
|
store.commit("setJWT", token);
|
||||||
store.commit("setSession", generateRandomCode(8));
|
store.commit("setSession", generateRandomCode(8));
|
||||||
|
@ -40,7 +38,6 @@ export async function login(username, password, recaptcha) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ const logoURL = `${staticURL}/img/logo.svg`;
|
||||||
const noAuth = window.FileBrowser.NoAuth;
|
const noAuth = window.FileBrowser.NoAuth;
|
||||||
const authMethod = window.FileBrowser.AuthMethod;
|
const authMethod = window.FileBrowser.AuthMethod;
|
||||||
const loginPage = window.FileBrowser.LoginPage;
|
const loginPage = window.FileBrowser.LoginPage;
|
||||||
const theme = window.FileBrowser.Theme;
|
|
||||||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||||
const resizePreview = window.FileBrowser.ResizePreview;
|
const resizePreview = window.FileBrowser.ResizePreview;
|
||||||
const enableExec = window.FileBrowser.EnableExec;
|
const enableExec = window.FileBrowser.EnableExec;
|
||||||
|
@ -30,7 +29,6 @@ export {
|
||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
authMethod,
|
||||||
loginPage,
|
loginPage,
|
||||||
theme,
|
|
||||||
enableThumbs,
|
enableThumbs,
|
||||||
resizePreview,
|
resizePreview,
|
||||||
enableExec,
|
enableExec,
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeaderBar from "@/components/header/HeaderBar";
|
|
||||||
|
|
||||||
const errors = {
|
const errors = {
|
||||||
0: {
|
0: {
|
||||||
|
@ -32,7 +31,6 @@ const errors = {
|
||||||
export default {
|
export default {
|
||||||
name: "errors",
|
name: "errors",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
|
||||||
},
|
},
|
||||||
props: ["errorCode", "showHeader"],
|
props: ["errorCode", "showHeader"],
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -80,10 +80,10 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("keydown", this.keyEvent);
|
window.addEventListener("keydown", this.keyEvent);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
},
|
},
|
||||||
destroyed() {
|
unmounted() {
|
||||||
if (this.$store.state.showShell) {
|
if (this.$store.state.showShell) {
|
||||||
this.$store.commit("toggleShell");
|
this.$store.commit("toggleShell");
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ export default {
|
||||||
},
|
},
|
||||||
currentView(newView) {
|
currentView(newView) {
|
||||||
// Commit the new value to the store
|
// Commit the new value to the store
|
||||||
this.setCurrentValue(this.newValue);
|
this.setCurrentValue(newView);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(["setLoading","setCurrentView"]),
|
...mapMutations(["setLoading","setCurrentView"]),
|
||||||
|
|
|
@ -7,10 +7,10 @@
|
||||||
<editorBar v-else-if="currentView === 'editor'"></editorBar>
|
<editorBar v-else-if="currentView === 'editor'"></editorBar>
|
||||||
<defaultBar v-else></defaultBar>
|
<defaultBar v-else></defaultBar>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
<main>
|
<main :class="{ 'dark-mode': isDarkMode }">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</main>
|
</main>
|
||||||
<prompts></prompts>
|
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||||
<upload-files></upload-files>
|
<upload-files></upload-files>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -20,7 +20,6 @@ import editorBar from "./bars/EditorBar.vue"
|
||||||
import defaultBar from "./bars/Default.vue"
|
import defaultBar from "./bars/Default.vue"
|
||||||
import listingBar from "./bars/ListingBar.vue"
|
import listingBar from "./bars/ListingBar.vue"
|
||||||
import Prompts from "@/components/prompts/Prompts";
|
import Prompts from "@/components/prompts/Prompts";
|
||||||
import Action from "@/components/header/Action";
|
|
||||||
import { mapState, mapGetters } from "vuex";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import Sidebar from "@/components/Sidebar.vue";
|
import Sidebar from "@/components/Sidebar.vue";
|
||||||
import UploadFiles from "../components/prompts/UploadFiles";
|
import UploadFiles from "../components/prompts/UploadFiles";
|
||||||
|
@ -31,7 +30,6 @@ export default {
|
||||||
defaultBar,
|
defaultBar,
|
||||||
editorBar,
|
editorBar,
|
||||||
listingBar,
|
listingBar,
|
||||||
Action,
|
|
||||||
Sidebar,
|
Sidebar,
|
||||||
Prompts,
|
Prompts,
|
||||||
UploadFiles,
|
UploadFiles,
|
||||||
|
@ -47,7 +45,9 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["isLogged", "progress", "isListing"]),
|
...mapGetters(["isLogged", "progress", "isListing"]),
|
||||||
...mapState(["req", "user", "state"]),
|
...mapState(["req", "user", "state"]),
|
||||||
|
isDarkMode() {
|
||||||
|
return this.user.darkMode === true
|
||||||
|
},
|
||||||
isExecEnabled: () => enableExec,
|
isExecEnabled: () => enableExec,
|
||||||
currentView() {
|
currentView() {
|
||||||
if (this.req.type == undefined) {
|
if (this.req.type == undefined) {
|
||||||
|
@ -83,4 +83,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
/* Use the class .dark-mode to apply styles conditionally */
|
||||||
|
.dark-mode {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
<div class="wrapper">
|
<div v-if="settingsEnabled" class="wrapper">
|
||||||
<ul>
|
<ul>
|
||||||
<router-link to="/settings/profile"
|
<router-link to="/settings/profile"
|
||||||
><li :class="{ active: $route.path === '/settings/profile' }">
|
><li :class="{ active: $route.path === '/settings/profile' }">
|
||||||
|
@ -57,7 +57,10 @@ export default {
|
||||||
this.$store.commit("updateRequest", { name: "Settings" });
|
this.$store.commit("updateRequest", { name: "Settings" });
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user", "loading","req"]),
|
...mapState(["user"]),
|
||||||
|
settingsEnabled() {
|
||||||
|
return this.user.disableSettings == false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
|
@ -160,8 +160,6 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||||
import { pub as api } from "@/api";
|
import { pub as api } from "@/api";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import HeaderBar from "@/components/header/HeaderBar";
|
|
||||||
import Action from "@/components/header/Action";
|
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
import Errors from "@/views/Errors";
|
import Errors from "@/views/Errors";
|
||||||
import QrcodeVue from "qrcode.vue";
|
import QrcodeVue from "qrcode.vue";
|
||||||
|
@ -171,8 +169,6 @@ import Clipboard from "clipboard";
|
||||||
export default {
|
export default {
|
||||||
name: "share",
|
name: "share",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
|
||||||
Action,
|
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Item,
|
Item,
|
||||||
QrcodeVue,
|
QrcodeVue,
|
||||||
|
@ -206,7 +202,7 @@ export default {
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
this.$showSuccess(this.$t("success.linkCopied"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
this.clip.destroy();
|
this.clip.destroy();
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,24 +18,18 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import { users, files as api } from "@/api";
|
import { users, files as api } from "@/api";
|
||||||
import { enableExec } from "@/utils/constants";
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import css from "@/utils/css";
|
import css from "@/utils/css";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import Search from "@/components/Search.vue";
|
|
||||||
|
|
||||||
import Item from "@/components/files/ListingItem.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listing",
|
name: "listing",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
Action,
|
Action,
|
||||||
Search,
|
|
||||||
Item,
|
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
@ -44,6 +38,7 @@ export default {
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
itemWeight: 0,
|
itemWeight: 0,
|
||||||
|
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -112,8 +107,9 @@ export default {
|
||||||
viewIcon() {
|
viewIcon() {
|
||||||
const icons = {
|
const icons = {
|
||||||
list: "view_module",
|
list: "view_module",
|
||||||
mosaic: "grid_view",
|
compact: "view_module",
|
||||||
"mosaic gallery": "view_list",
|
normal: "grid_view",
|
||||||
|
gallery: "view_list",
|
||||||
};
|
};
|
||||||
return icons[this.user.viewMode];
|
return icons[this.user.viewMode];
|
||||||
},
|
},
|
||||||
|
@ -166,7 +162,7 @@ export default {
|
||||||
document.addEventListener("dragleave", this.dragLeave);
|
document.addEventListener("dragleave", this.dragLeave);
|
||||||
document.addEventListener("drop", this.drop);
|
document.addEventListener("drop", this.drop);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
// Remove event listeners before destroying this page.
|
// Remove event listeners before destroying this page.
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
window.removeEventListener("scroll", this.scrollEvent);
|
window.removeEventListener("scroll", this.scrollEvent);
|
||||||
|
@ -265,21 +261,14 @@ export default {
|
||||||
},
|
},
|
||||||
switchView: async function () {
|
switchView: async function () {
|
||||||
this.$store.commit("closeHovers");
|
this.$store.commit("closeHovers");
|
||||||
const modes = {
|
const currentIndex = this.viewModes.indexOf(this.user.viewMode);
|
||||||
list: "mosaic",
|
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||||
mosaic: "mosaic gallery",
|
|
||||||
"mosaic gallery": "list",
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id: this.user.id,
|
id: this.user.id,
|
||||||
viewMode: modes[this.user.viewMode] || "list",
|
viewMode: this.viewModes[nextIndex],
|
||||||
};
|
};
|
||||||
//users.update(data, ["viewMode"]).catch(this.$showError);
|
users.update(data, ["viewMode"]).catch(this.$showError);
|
||||||
this.$store.commit("updateUser", data);
|
this.$store.commit("updateUser", data);
|
||||||
|
|
||||||
//this.setItemWeight();
|
|
||||||
//this.fillWindow();
|
|
||||||
},
|
},
|
||||||
preventDefault(event) {
|
preventDefault(event) {
|
||||||
// Wrapper around prevent default.
|
// Wrapper around prevent default.
|
||||||
|
@ -381,7 +370,7 @@ export default {
|
||||||
let columns = Math.floor(
|
let columns = Math.floor(
|
||||||
document.querySelector("main").offsetWidth / this.columnWidth
|
document.querySelector("main").offsetWidth / this.columnWidth
|
||||||
);
|
);
|
||||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
let items = css(["#listing .item", "#listing .item"]);
|
||||||
if (columns === 0) columns = 1;
|
if (columns === 0) columns = 1;
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||||
},
|
},
|
||||||
|
@ -589,7 +578,6 @@ export default {
|
||||||
}
|
}
|
||||||
this.$store.commit("updateRequest", {});
|
this.$store.commit("updateRequest", {});
|
||||||
let uri = url.removeLastDir(this.$route.path) + "/";
|
let uri = url.removeLastDir(this.$route.path) + "/";
|
||||||
console.log(url)
|
|
||||||
this.$router.push({ path: uri });
|
this.$router.push({ path: uri });
|
||||||
},
|
},
|
||||||
upload: function () {
|
upload: function () {
|
||||||
|
|
|
@ -30,14 +30,12 @@ import url from "@/utils/url";
|
||||||
|
|
||||||
import HeaderBar from "@/components/header/HeaderBar";
|
import HeaderBar from "@/components/header/HeaderBar";
|
||||||
import Action from "@/components/header/Action";
|
import Action from "@/components/header/Action";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "editor",
|
name: "editor",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
Action,
|
Action,
|
||||||
Breadcrumbs,
|
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {};
|
return {};
|
||||||
|
@ -77,7 +75,7 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("keydown", this.keyEvent);
|
window.addEventListener("keydown", this.keyEvent);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,13 +30,11 @@ import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import { users, files as api } from "@/api";
|
import { users, files as api } from "@/api";
|
||||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||||
import Action from "@/components/header/Action.vue";
|
import Action from "@/components/header/Action.vue";
|
||||||
import url from "@/utils/url";
|
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import css from "@/utils/css";
|
import css from "@/utils/css";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import Search from "@/components/Search.vue";
|
import Search from "@/components/Search.vue";
|
||||||
|
|
||||||
import Item from "@/components/files/ListingItem.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listing",
|
name: "listing",
|
||||||
|
@ -44,7 +42,6 @@ export default {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
Action,
|
Action,
|
||||||
Search,
|
Search,
|
||||||
Item,
|
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
@ -53,6 +50,7 @@ export default {
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
itemWeight: 0,
|
itemWeight: 0,
|
||||||
|
viewModes: ['list', 'compact', 'normal', 'gallery'],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -118,8 +116,9 @@ export default {
|
||||||
viewIcon() {
|
viewIcon() {
|
||||||
const icons = {
|
const icons = {
|
||||||
list: "view_module",
|
list: "view_module",
|
||||||
mosaic: "grid_view",
|
compact: "view_module",
|
||||||
"mosaic gallery": "view_list",
|
normal: "grid_view",
|
||||||
|
gallery: "view_list",
|
||||||
};
|
};
|
||||||
return icons[this.user.viewMode];
|
return icons[this.user.viewMode];
|
||||||
},
|
},
|
||||||
|
@ -172,7 +171,7 @@ export default {
|
||||||
document.addEventListener("dragleave", this.dragLeave);
|
document.addEventListener("dragleave", this.dragLeave);
|
||||||
document.addEventListener("drop", this.drop);
|
document.addEventListener("drop", this.drop);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
// Remove event listeners before destroying this page.
|
// Remove event listeners before destroying this page.
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
window.removeEventListener("scroll", this.scrollEvent);
|
window.removeEventListener("scroll", this.scrollEvent);
|
||||||
|
@ -271,21 +270,14 @@ export default {
|
||||||
},
|
},
|
||||||
switchView: async function () {
|
switchView: async function () {
|
||||||
this.$store.commit("closeHovers");
|
this.$store.commit("closeHovers");
|
||||||
const modes = {
|
const currentIndex = this.viewModes.indexOf(this.user.viewMode);
|
||||||
list: "mosaic",
|
const nextIndex = (currentIndex + 1) % this.viewModes.length;
|
||||||
mosaic: "mosaic gallery",
|
|
||||||
"mosaic gallery": "list",
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id: this.user.id,
|
id: this.user.id,
|
||||||
viewMode: modes[this.user.viewMode] || "list",
|
viewMode: this.viewModes[nextIndex],
|
||||||
};
|
};
|
||||||
//users.update(data, ["viewMode"]).catch(this.$showError);
|
users.update(data, ["viewMode"]).catch(this.$showError);
|
||||||
this.$store.commit("updateUser", data);
|
this.$store.commit("updateUser", data);
|
||||||
|
|
||||||
//this.setItemWeight();
|
|
||||||
//this.fillWindow();
|
|
||||||
},
|
},
|
||||||
preventDefault(event) {
|
preventDefault(event) {
|
||||||
// Wrapper around prevent default.
|
// Wrapper around prevent default.
|
||||||
|
@ -387,7 +379,7 @@ export default {
|
||||||
let columns = Math.floor(
|
let columns = Math.floor(
|
||||||
document.querySelector("main").offsetWidth / this.columnWidth
|
document.querySelector("main").offsetWidth / this.columnWidth
|
||||||
);
|
);
|
||||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
let items = css(["#listing .item", "#listing .item"]);
|
||||||
if (columns === 0) columns = 1;
|
if (columns === 0) columns = 1;
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import { theme } from "@/utils/constants";
|
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import ace from "ace-builds/src-min-noconflict/ace.js";
|
import ace from "ace-builds/src-min-noconflict/ace.js";
|
||||||
|
@ -53,7 +52,7 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("keydown", this.keyEvent);
|
window.addEventListener("keydown", this.keyEvent);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
},
|
},
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="listing" ref="listing" :class="user.viewMode + ' file-icons'">
|
<div v-else id="listing" ref="listing" :class="listingViewMode + ' file-icons'">
|
||||||
<div>
|
<div>
|
||||||
<div class="item header">
|
<div class="item header">
|
||||||
<div></div>
|
<div></div>
|
||||||
|
@ -132,8 +132,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="req.numDirs > 0">
|
||||||
<h2 v-if="req.numDirs > 0">{{ $t("files.folders") }}</h2>
|
<div class="header-items">
|
||||||
|
<h2>{{ $t("files.folders") }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="req.numDirs > 0">
|
<div v-if="req.numDirs > 0">
|
||||||
<item
|
<item
|
||||||
v-for="item in dirs"
|
v-for="item in dirs"
|
||||||
|
@ -150,7 +153,11 @@
|
||||||
</item>
|
</item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="req.numFiles > 0">{{ $t("files.files") }}</h2>
|
<div v-if="req.numFiles > 0">
|
||||||
|
<div class="header-items">
|
||||||
|
<h2>{{ $t("files.files") }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="req.numFiles > 0">
|
<div v-if="req.numFiles > 0">
|
||||||
<item
|
<item
|
||||||
v-for="item in files"
|
v-for="item in files"
|
||||||
|
@ -201,11 +208,19 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.header-items {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import { users, files as api } from "@/api";
|
import { users, files as api } from "@/api";
|
||||||
import { enableExec } from "@/utils/constants";
|
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
import css from "@/utils/css";
|
import css from "@/utils/css";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
@ -291,11 +306,15 @@ export default {
|
||||||
viewIcon() {
|
viewIcon() {
|
||||||
const icons = {
|
const icons = {
|
||||||
list: "view_module",
|
list: "view_module",
|
||||||
mosaic: "grid_view",
|
compact: "view_module",
|
||||||
"mosaic gallery": "view_list",
|
normal: "grid_view",
|
||||||
|
gallery: "view_list",
|
||||||
};
|
};
|
||||||
return icons[this.user.viewMode];
|
return icons[this.user.viewMode];
|
||||||
},
|
},
|
||||||
|
listingViewMode() {
|
||||||
|
return this.user.viewMode
|
||||||
|
},
|
||||||
headerButtons() {
|
headerButtons() {
|
||||||
return {
|
return {
|
||||||
select: this.selectedCount > 0,
|
select: this.selectedCount > 0,
|
||||||
|
@ -345,7 +364,7 @@ export default {
|
||||||
document.addEventListener("dragleave", this.dragLeave);
|
document.addEventListener("dragleave", this.dragLeave);
|
||||||
document.addEventListener("drop", this.drop);
|
document.addEventListener("drop", this.drop);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
// Remove event listeners before destroying this page.
|
// Remove event listeners before destroying this page.
|
||||||
window.removeEventListener("keydown", this.keyEvent);
|
window.removeEventListener("keydown", this.keyEvent);
|
||||||
window.removeEventListener("scroll", this.scrollEvent);
|
window.removeEventListener("scroll", this.scrollEvent);
|
||||||
|
@ -528,7 +547,7 @@ export default {
|
||||||
let columns = Math.floor(
|
let columns = Math.floor(
|
||||||
document.querySelector("main").offsetWidth / this.columnWidth
|
document.querySelector("main").offsetWidth / this.columnWidth
|
||||||
);
|
);
|
||||||
let items = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
|
let items = css(["#listing .item", "#listing .item"]);
|
||||||
if (columns === 0) columns = 1;
|
if (columns === 0) columns = 1;
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`;
|
items.style.width = `calc(${100 / columns}% - 1em)`;
|
||||||
},
|
},
|
||||||
|
|
|
@ -106,8 +106,6 @@ import { files as api } from "@/api";
|
||||||
import { resizePreview } from "@/utils/constants";
|
import { resizePreview } from "@/utils/constants";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import HeaderBar from "@/components/header/HeaderBar";
|
|
||||||
import Action from "@/components/header/Action";
|
|
||||||
import ExtendedImage from "@/components/files/ExtendedImage";
|
import ExtendedImage from "@/components/files/ExtendedImage";
|
||||||
|
|
||||||
const mediaTypes = ["image", "video", "audio", "blob"];
|
const mediaTypes = ["image", "video", "audio", "blob"];
|
||||||
|
@ -115,8 +113,6 @@ const mediaTypes = ["image", "video", "audio", "blob"];
|
||||||
export default {
|
export default {
|
||||||
name: "preview",
|
name: "preview",
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
|
||||||
Action,
|
|
||||||
ExtendedImage,
|
ExtendedImage,
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -176,7 +172,7 @@ export default {
|
||||||
this.listing = this.oldReq.items;
|
this.listing = this.oldReq.items;
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.key);
|
window.removeEventListener("keydown", this.key);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -71,15 +71,6 @@
|
||||||
{{ $t("settings.disableUsedDiskPercentage") }}
|
{{ $t("settings.disableUsedDiskPercentage") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
|
||||||
<label for="theme">{{ $t("settings.themes.title") }}</label>
|
|
||||||
<themes
|
|
||||||
class="input input--block"
|
|
||||||
:theme.sync="settings.frontend.theme"
|
|
||||||
id="theme"
|
|
||||||
></themes>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
<label for="branding-name">{{ $t("settings.instanceName") }}</label>
|
||||||
<input
|
<input
|
||||||
|
@ -194,13 +185,11 @@ import { settings as api } from "@/api";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
import UserForm from "@/components/settings/UserForm";
|
import UserForm from "@/components/settings/UserForm";
|
||||||
import Rules from "@/components/settings/Rules";
|
import Rules from "@/components/settings/Rules";
|
||||||
import Themes from "@/components/settings/Themes";
|
|
||||||
import Errors from "@/views/Errors";
|
import Errors from "@/views/Errors";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "settings",
|
name: "settings",
|
||||||
components: {
|
components: {
|
||||||
Themes,
|
|
||||||
UserForm,
|
UserForm,
|
||||||
Rules,
|
Rules,
|
||||||
Errors,
|
Errors,
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
<input type="checkbox" v-model="darkMode" />
|
||||||
|
Dark Mode
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="hideDotfiles" />
|
<input type="checkbox" v-model="hideDotfiles" />
|
||||||
{{ $t("settings.hideDotfiles") }}
|
{{ $t("settings.hideDotfiles") }}
|
||||||
|
@ -19,6 +23,11 @@
|
||||||
<input type="checkbox" v-model="dateFormat" />
|
<input type="checkbox" v-model="dateFormat" />
|
||||||
{{ $t("settings.setDateFormat") }}
|
{{ $t("settings.setDateFormat") }}
|
||||||
</p>
|
</p>
|
||||||
|
<h3>Listing View Style</h3>
|
||||||
|
<ViewMode
|
||||||
|
class="input input--block"
|
||||||
|
:viewMode.sync="viewMode"
|
||||||
|
></ViewMode>
|
||||||
<h3>{{ $t("settings.language") }}</h3>
|
<h3>{{ $t("settings.language") }}</h3>
|
||||||
<languages
|
<languages
|
||||||
class="input input--block"
|
class="input input--block"
|
||||||
|
@ -75,11 +84,13 @@
|
||||||
import { mapState, mapMutations } from "vuex";
|
import { mapState, mapMutations } from "vuex";
|
||||||
import { users as api } from "@/api";
|
import { users as api } from "@/api";
|
||||||
import Languages from "@/components/settings/Languages";
|
import Languages from "@/components/settings/Languages";
|
||||||
|
import ViewMode from "@/components/settings/ViewMode";
|
||||||
import i18n, { rtlLanguages } from "@/i18n";
|
import i18n, { rtlLanguages } from "@/i18n";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "settings",
|
name: "settings",
|
||||||
components: {
|
components: {
|
||||||
|
ViewMode,
|
||||||
Languages,
|
Languages,
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -89,6 +100,8 @@ export default {
|
||||||
hideDotfiles: false,
|
hideDotfiles: false,
|
||||||
singleClick: false,
|
singleClick: false,
|
||||||
dateFormat: false,
|
dateFormat: false,
|
||||||
|
darkMode: false,
|
||||||
|
viewMode: "list",
|
||||||
locale: "",
|
locale: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -109,8 +122,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
if (typeof this.user.darkMode === 'undefined') {
|
||||||
|
this.darkMode = false;
|
||||||
|
} else {
|
||||||
|
this.darkMode = this.user.darkMode
|
||||||
|
}
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
this.locale = this.user.locale;
|
this.locale = this.user.locale;
|
||||||
|
this.viewMode = this.user.viewMode;
|
||||||
this.hideDotfiles = this.user.hideDotfiles;
|
this.hideDotfiles = this.user.hideDotfiles;
|
||||||
this.singleClick = this.user.singleClick;
|
this.singleClick = this.user.singleClick;
|
||||||
this.dateFormat = this.user.dateFormat;
|
this.dateFormat = this.user.dateFormat;
|
||||||
|
@ -135,11 +154,12 @@ export default {
|
||||||
},
|
},
|
||||||
async updateSettings(event) {
|
async updateSettings(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
id: this.user.id,
|
id: this.user.id,
|
||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
|
darkMode: this.darkMode,
|
||||||
|
viewMode: this.viewMode,
|
||||||
hideDotfiles: this.hideDotfiles,
|
hideDotfiles: this.hideDotfiles,
|
||||||
singleClick: this.singleClick,
|
singleClick: this.singleClick,
|
||||||
dateFormat: this.dateFormat,
|
dateFormat: this.dateFormat,
|
||||||
|
@ -149,6 +169,8 @@ export default {
|
||||||
rtlLanguages.includes(i18n.locale);
|
rtlLanguages.includes(i18n.locale);
|
||||||
await api.update(data, [
|
await api.update(data, [
|
||||||
"locale",
|
"locale",
|
||||||
|
"darkMode",
|
||||||
|
"viewMode",
|
||||||
"hideDotfiles",
|
"hideDotfiles",
|
||||||
"singleClick",
|
"singleClick",
|
||||||
"dateFormat",
|
"dateFormat",
|
||||||
|
|
|
@ -107,7 +107,7 @@ export default {
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
this.$showSuccess(this.$t("success.linkCopied"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
this.clip.destroy();
|
this.clip.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
Loading…
Reference in New Issue