Merge branch 'v0.2.1'
This commit is contained in:
		
						commit
						aab3224d2c
					
				| 
						 | 
				
			
			@ -4,8 +4,9 @@ on:
 | 
			
		|||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'main'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
  test-backend:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +14,16 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -21,8 +31,18 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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:
 | 
			
		||||
    needs: [lint, test]
 | 
			
		||||
    needs: [lint-frontend, lint-backend, test-backend, format-backend]
 | 
			
		||||
    name: Push latest
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,10 @@ on:
 | 
			
		|||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'main'
 | 
			
		||||
      - 'v\d+.\d+.\d+'
 | 
			
		||||
      - 'v[0-9]+.[0-9]+.[0-9]+'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
  test-backend:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +15,16 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -22,8 +32,18 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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:
 | 
			
		||||
    needs: [lint, test]
 | 
			
		||||
    needs: [lint-frontend, lint-backend, test-backend, format-backend]
 | 
			
		||||
    name: Push PR
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,9 +3,9 @@ name: release
 | 
			
		|||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'v\d+.\d+.\d+'
 | 
			
		||||
      - 'v[0-9]+.[0-9]+.[0-9]+'
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
  test-backend:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,16 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +30,19 @@ jobs:
 | 
			
		|||
        with:
 | 
			
		||||
          go-version: 1.21.1
 | 
			
		||||
      - 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:
 | 
			
		||||
    needs: [lint, test]
 | 
			
		||||
    needs: [lint-frontend, lint-backend, test-backend, format-backend]
 | 
			
		||||
    name: Push release
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: ${{ github.event_name == 'release' }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +67,13 @@ jobs:
 | 
			
		|||
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
 | 
			
		||||
        with:
 | 
			
		||||
          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
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
| 
						 | 
				
			
			@ -57,5 +81,5 @@ jobs:
 | 
			
		|||
          platforms: linux/amd64,linux/arm64,linux/arm/v7
 | 
			
		||||
          file: ./Dockerfile
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          tags: ${{ steps.modify-json.outputs.cleaned_tag }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
							
								
								
									
										12
									
								
								Dockerfile
								
								
								
								
							
							
						
						
									
										12
									
								
								Dockerfile
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
FROM node:slim as nbuild
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY  ./frontend/package*.json ./
 | 
			
		||||
RUN npm i
 | 
			
		||||
RUN npm ci --maxsockets 1
 | 
			
		||||
COPY  ./frontend/ ./
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,13 +12,11 @@ RUN go get -u golang.org/x/net
 | 
			
		|||
RUN go build -ldflags="-w -s" -o filebrowser .
 | 
			
		||||
 | 
			
		||||
FROM alpine:latest
 | 
			
		||||
ARG app="/app/filebrowser"
 | 
			
		||||
RUN apk --no-cache add \
 | 
			
		||||
      ca-certificates \
 | 
			
		||||
      mailcap
 | 
			
		||||
VOLUME /srv
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
WORKDIR /
 | 
			
		||||
COPY --from=base /app/filebrowser.yaml /filebrowser.yaml
 | 
			
		||||
COPY --from=base /app/filebrowser /filebrowser
 | 
			
		||||
COPY --from=nbuild /app/dist/ /frontend/dist/
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=base $app* ./
 | 
			
		||||
COPY --from=nbuild /app/dist/ ./frontend/dist/
 | 
			
		||||
ENTRYPOINT [ "./filebrowser" ]
 | 
			
		||||
| 
						 | 
				
			
			@ -146,14 +146,11 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if u == nil {
 | 
			
		||||
		pass, err := users.HashPwd(a.Cred.Password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		log.Println("creds", a.Cred.Password)
 | 
			
		||||
		// create user with the provided credentials
 | 
			
		||||
		d := &users.User{
 | 
			
		||||
			Username:     a.Cred.Username,
 | 
			
		||||
			Password:     pass,
 | 
			
		||||
			Password:     a.Cred.Password,
 | 
			
		||||
			Scope:        a.Settings.UserDefaults.Scope,
 | 
			
		||||
			Locale:       a.Settings.UserDefaults.Locale,
 | 
			
		||||
			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 {
 | 
			
		||||
		u = a.GetUser(u)
 | 
			
		||||
 | 
			
		||||
		// update the password when it doesn't match the current
 | 
			
		||||
		if p {
 | 
			
		||||
			pass, err := users.HashPwd(a.Cred.Password)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			u.Password = pass
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// update user with provided fields
 | 
			
		||||
		err := a.Users.Update(u)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -201,31 +188,31 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
 | 
			
		|||
// GetUser returns a User filled with hook values or provided defaults
 | 
			
		||||
func (a *HookAuth) GetUser(d *users.User) *users.User {
 | 
			
		||||
	// adds all permissions when user is admin
 | 
			
		||||
	isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
 | 
			
		||||
	isAdmin := d.Perm.Admin
 | 
			
		||||
	perms := users.Permissions{
 | 
			
		||||
		Admin:    isAdmin,
 | 
			
		||||
		Execute:  isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
 | 
			
		||||
		Create:   isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
 | 
			
		||||
		Rename:   isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
 | 
			
		||||
		Modify:   isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
 | 
			
		||||
		Delete:   isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
 | 
			
		||||
		Share:    isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
 | 
			
		||||
		Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
 | 
			
		||||
		Execute:  isAdmin || d.Perm.Execute,
 | 
			
		||||
		Create:   isAdmin || d.Perm.Create,
 | 
			
		||||
		Rename:   isAdmin || d.Perm.Rename,
 | 
			
		||||
		Modify:   isAdmin || d.Perm.Modify,
 | 
			
		||||
		Delete:   isAdmin || d.Perm.Delete,
 | 
			
		||||
		Share:    isAdmin || d.Perm.Share,
 | 
			
		||||
		Download: isAdmin || d.Perm.Download,
 | 
			
		||||
	}
 | 
			
		||||
	user := users.User{
 | 
			
		||||
		ID:          d.ID,
 | 
			
		||||
		Username:    d.Username,
 | 
			
		||||
		Password:    d.Password,
 | 
			
		||||
		Scope:       a.Fields.GetString("user.scope", d.Scope),
 | 
			
		||||
		Locale:      a.Fields.GetString("user.locale", d.Locale),
 | 
			
		||||
		Scope:       d.Scope,
 | 
			
		||||
		Locale:      d.Locale,
 | 
			
		||||
		ViewMode:    d.ViewMode,
 | 
			
		||||
		SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
 | 
			
		||||
		SingleClick: d.SingleClick,
 | 
			
		||||
		Sorting: files.Sorting{
 | 
			
		||||
			Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
 | 
			
		||||
			By:  a.Fields.GetString("user.sorting.by", d.Sorting.By),
 | 
			
		||||
			Asc: d.Sorting.Asc,
 | 
			
		||||
			By:  d.Sorting.By,
 | 
			
		||||
		},
 | 
			
		||||
		Commands:     a.Fields.GetArray("user.commands", d.Commands),
 | 
			
		||||
		HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
 | 
			
		||||
		Commands:     d.Commands,
 | 
			
		||||
		HideDotfiles: d.HideDotfiles,
 | 
			
		||||
		Perm:         perms,
 | 
			
		||||
		LockPassword: true,
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,6 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store) (*users.User, error) {
 | 
			
		|||
	if r.Body == nil {
 | 
			
		||||
		return nil, os.ErrPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := json.NewDecoder(r.Body).Decode(&cred)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, os.ErrPermission
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,22 +16,16 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	lumberjack "gopkg.in/natefinch/lumberjack.v2"
 | 
			
		||||
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/auth"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/diskcache"
 | 
			
		||||
	fbhttp "github.com/gtsteffaniak/filebrowser/http"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/img"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/search"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/index"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/settings"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/storage"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	configFile string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type dirFS struct {
 | 
			
		||||
	http.Dir
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +64,7 @@ var rootCmd = &cobra.Command{
 | 
			
		|||
			fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
 | 
			
		||||
		}
 | 
			
		||||
		// 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)
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
		var listener net.Listener
 | 
			
		||||
| 
						 | 
				
			
			@ -124,41 +117,17 @@ func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfac
 | 
			
		|||
	os.Exit(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:gocyclo
 | 
			
		||||
func getRunParams(st *storage.Storage) *settings.Server {
 | 
			
		||||
	server, err := st.Settings.GetServer()
 | 
			
		||||
	checkErr(err)
 | 
			
		||||
	return server
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupLog(logMethod string) {
 | 
			
		||||
	switch logMethod {
 | 
			
		||||
	case "stdout":
 | 
			
		||||
		log.SetOutput(os.Stdout)
 | 
			
		||||
	case "stderr":
 | 
			
		||||
		log.SetOutput(os.Stderr)
 | 
			
		||||
	case "":
 | 
			
		||||
		log.SetOutput(io.Discard)
 | 
			
		||||
	default:
 | 
			
		||||
		log.SetOutput(&lumberjack.Logger{
 | 
			
		||||
			Filename:   logMethod,
 | 
			
		||||
			MaxSize:    100,
 | 
			
		||||
			MaxAge:     14,
 | 
			
		||||
			MaxBackups: 10,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func quickSetup(d pythonData) {
 | 
			
		||||
	settings.GlobalConfiguration.Key = generateKey()
 | 
			
		||||
	var err error
 | 
			
		||||
	if settings.GlobalConfiguration.Auth.Method == "noauth" {
 | 
			
		||||
		err = d.store.Auth.Save(&auth.NoAuth{})
 | 
			
		||||
		err := d.store.Auth.Save(&auth.NoAuth{})
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
	} else {
 | 
			
		||||
		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)
 | 
			
		||||
	err = d.store.Settings.SaveServer(&settings.GlobalConfiguration.Server)
 | 
			
		||||
	checkErr(err)
 | 
			
		||||
| 
						 | 
				
			
			@ -168,12 +137,26 @@ func quickSetup(d pythonData) {
 | 
			
		|||
		log.Fatal("username and password cannot be empty during quick setup")
 | 
			
		||||
	}
 | 
			
		||||
	user := &users.User{
 | 
			
		||||
		Username:     username,
 | 
			
		||||
		Password:     password,
 | 
			
		||||
		LockPassword: false,
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Password: password,
 | 
			
		||||
	}
 | 
			
		||||
	user.Perm.Admin = true
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
	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)
 | 
			
		||||
	checkErr(err)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,15 @@ var usersAddCmd = &cobra.Command{
 | 
			
		|||
	Long:  `Create a new user and add it to the database.`,
 | 
			
		||||
	Args:  cobra.ExactArgs(2), //nolint:gomnd
 | 
			
		||||
	Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
		password, err := users.HashPwd(args[1])
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
 | 
			
		||||
=======
 | 
			
		||||
>>>>>>> v0.2.1
 | 
			
		||||
		user := &users.User{
 | 
			
		||||
			Username:     args[0],
 | 
			
		||||
			Password:     password,
 | 
			
		||||
			Password:     args[1],
 | 
			
		||||
			LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
 | 
			
		||||
		}
 | 
			
		||||
		servSettings, err := d.store.Settings.GetServer()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,9 +8,12 @@ import (
 | 
			
		|||
 | 
			
		||||
func init() {
 | 
			
		||||
	usersCmd.AddCommand(usersUpdateCmd)
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
 | 
			
		||||
	usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
 | 
			
		||||
	usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
 | 
			
		||||
=======
 | 
			
		||||
>>>>>>> v0.2.1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var usersUpdateCmd = &cobra.Command{
 | 
			
		||||
| 
						 | 
				
			
			@ -21,9 +24,6 @@ options you want to change.`,
 | 
			
		|||
	Args: cobra.ExactArgs(1),
 | 
			
		||||
	Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
 | 
			
		||||
		username, id := parseUsernameOrID(args[0])
 | 
			
		||||
		flags := cmd.Flags()
 | 
			
		||||
		password := mustGetString(flags, "password")
 | 
			
		||||
		newUsername := mustGetString(flags, "username")
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			err  error
 | 
			
		||||
| 
						 | 
				
			
			@ -36,23 +36,6 @@ options you want to change.`,
 | 
			
		|||
			user, err = d.store.Users.Get("", username)
 | 
			
		||||
		}
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
		user.Scope = user.Scope
 | 
			
		||||
		user.Locale = user.Locale
 | 
			
		||||
		user.ViewMode = user.ViewMode
 | 
			
		||||
		user.SingleClick = user.SingleClick
 | 
			
		||||
		user.Perm = user.Perm
 | 
			
		||||
		user.Commands = user.Commands
 | 
			
		||||
		user.Sorting = user.Sorting
 | 
			
		||||
		user.LockPassword = user.LockPassword
 | 
			
		||||
 | 
			
		||||
		if newUsername != "" {
 | 
			
		||||
			user.Username = newUsername
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if password != "" {
 | 
			
		||||
			user.Password, err = users.HashPwd(password)
 | 
			
		||||
			checkErr(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = d.store.Users.Update(user)
 | 
			
		||||
		checkErr(err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,11 +3,9 @@ package cmd
 | 
			
		|||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/asdine/storm/v3"
 | 
			
		||||
	"github.com/goccy/go-yaml"
 | 
			
		||||
| 
						 | 
				
			
			@ -152,42 +150,3 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
 | 
			
		|||
		return errors.New("invalid format: " + ext)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
 | 
			
		||||
	result := make(map[string]interface{})
 | 
			
		||||
	for k, v := range in {
 | 
			
		||||
		result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cleanUpInterfaceArray(in []interface{}) []interface{} {
 | 
			
		||||
	result := make([]interface{}, len(in))
 | 
			
		||||
	for i, v := range in {
 | 
			
		||||
		result[i] = cleanUpMapValue(v)
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cleanUpMapValue(v interface{}) interface{} {
 | 
			
		||||
	switch v := v.(type) {
 | 
			
		||||
	case []interface{}:
 | 
			
		||||
		return cleanUpInterfaceArray(v)
 | 
			
		||||
	case map[interface{}]interface{}:
 | 
			
		||||
		return cleanUpInterfaceMap(v)
 | 
			
		||||
	default:
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
 | 
			
		||||
// then returns empty string array, else returns the splitted word array of cmd.
 | 
			
		||||
// This is to ensure the result will never be []string{""}
 | 
			
		||||
func convertCmdStrToCmdArray(cmd string) []string {
 | 
			
		||||
	var cmdArray []string
 | 
			
		||||
	trimmedCmdStr := strings.TrimSpace(cmd)
 | 
			
		||||
	if trimmedCmdStr != "" {
 | 
			
		||||
		cmdArray = strings.Split(trimmedCmdStr, " ")
 | 
			
		||||
	}
 | 
			
		||||
	return cmdArray
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,31 @@
 | 
			
		|||
server:
 | 
			
		||||
  port: 8080
 | 
			
		||||
  baseURL: "/"
 | 
			
		||||
  root: "/Users/steffag/git/go"
 | 
			
		||||
auth:
 | 
			
		||||
  method: noauth
 | 
			
		||||
  method: password
 | 
			
		||||
  signup: true
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
frontend:
 | 
			
		||||
  theme: dark
 | 
			
		||||
users:
 | 
			
		||||
  - name: admin
 | 
			
		||||
    settings:
 | 
			
		||||
      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/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/settings"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,20 +20,8 @@ const (
 | 
			
		|||
	TokenExpirationTime = time.Hour * 2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type userInfo struct {
 | 
			
		||||
	ID           uint              `json:"id"`
 | 
			
		||||
	Locale       string            `json:"locale"`
 | 
			
		||||
	ViewMode     string            `json:"viewMode"`
 | 
			
		||||
	SingleClick  bool              `json:"singleClick"`
 | 
			
		||||
	Perm         users.Permissions `json:"perm"`
 | 
			
		||||
	Commands     []string          `json:"commands"`
 | 
			
		||||
	LockPassword bool              `json:"lockPassword"`
 | 
			
		||||
	HideDotfiles bool              `json:"hideDotfiles"`
 | 
			
		||||
	DateFormat   bool              `json:"dateFormat"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type authToken struct {
 | 
			
		||||
	User userInfo `json:"user"`
 | 
			
		||||
	User users.User `json:"user"`
 | 
			
		||||
	jwt.RegisteredClaims
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +132,9 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
 | 
			
		|||
 | 
			
		||||
	user := &users.User{
 | 
			
		||||
		Username: info.Username,
 | 
			
		||||
		Password: info.Password,
 | 
			
		||||
	}
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
 | 
			
		||||
	pwd, err := users.HashPwd(info.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +143,9 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
 | 
			
		|||
 | 
			
		||||
	user.Password = pwd
 | 
			
		||||
 | 
			
		||||
=======
 | 
			
		||||
	settings.GlobalConfiguration.UserDefaults.Apply(user)
 | 
			
		||||
>>>>>>> v0.2.1
 | 
			
		||||
	userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
	claims := &authToken{
 | 
			
		||||
		User: userInfo{
 | 
			
		||||
			ID:           user.ID,
 | 
			
		||||
			Locale:       user.Locale,
 | 
			
		||||
			ViewMode:     user.ViewMode,
 | 
			
		||||
			SingleClick:  user.SingleClick,
 | 
			
		||||
			Perm:         user.Perm,
 | 
			
		||||
			LockPassword: user.LockPassword,
 | 
			
		||||
			Commands:     user.Commands,
 | 
			
		||||
			HideDotfiles: user.HideDotfiles,
 | 
			
		||||
			DateFormat:   user.DateFormat,
 | 
			
		||||
		},
 | 
			
		||||
		User: *user,
 | 
			
		||||
		RegisteredClaims: jwt.RegisteredClaims{
 | 
			
		||||
			IssuedAt:  jwt.NewNumericDate(time.Now()),
 | 
			
		||||
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,17 +3,17 @@ package http
 | 
			
		|||
import (
 | 
			
		||||
	"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) {
 | 
			
		||||
	response := []map[string]interface{}{}
 | 
			
		||||
	query := r.URL.Query().Get("query")
 | 
			
		||||
 | 
			
		||||
	// Retrieve the User-Agent and X-Auth headers from the request
 | 
			
		||||
	sessionId := r.Header.Get("SessionId")
 | 
			
		||||
	indexInfo, fileTypes := search.SearchAllIndexes(query, r.URL.Path, sessionId)
 | 
			
		||||
	for _, path := range indexInfo {
 | 
			
		||||
	index := *index.GetIndex()
 | 
			
		||||
	results, fileTypes := index.Search(query, r.URL.Path, sessionId)
 | 
			
		||||
	for _, path := range results {
 | 
			
		||||
		responseObj := map[string]interface{}{
 | 
			
		||||
			"path": path,
 | 
			
		||||
			"dir":  true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,7 +129,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
 | 
			
		|||
 | 
			
		||||
	var token string
 | 
			
		||||
	if len(hash) > 0 {
 | 
			
		||||
		tokenBuffer := make([]byte, 96) //nolint:gomnd
 | 
			
		||||
		tokenBuffer := make([]byte, 24) //nolint:gomnd
 | 
			
		||||
		if _, err := rand.Read(tokenBuffer); err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, err
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,7 +40,6 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
 | 
			
		|||
		"LoginPage":             auther.LoginPage(),
 | 
			
		||||
		"CSS":                   false,
 | 
			
		||||
		"ReCaptcha":             false,
 | 
			
		||||
		"Theme":                 d.settings.Frontend.Theme,
 | 
			
		||||
		"EnableThumbs":          d.server.EnableThumbnails,
 | 
			
		||||
		"ResizePreview":         d.server.ResizePreview,
 | 
			
		||||
		"EnableExec":            d.server.EnableExec,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,11 +124,6 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
 | 
			
		|||
		return http.StatusBadRequest, errors.ErrEmptyPassword
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Data.Password, err = users.HashPwd(req.Data.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +179,6 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
 | 
			
		|||
			if !d.user.Perm.Admin && d.user.LockPassword {
 | 
			
		||||
				return http.StatusForbidden, nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			req.Data.Password, err = users.HashPwd(req.Data.Password)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return http.StatusInternalServerError, err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
package search
 | 
			
		||||
package index
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mime"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
| 
						 | 
				
			
			@ -120,3 +121,35 @@ func updateSize(given string) int {
 | 
			
		|||
		return size
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsMatchingType(extension string, matchType string) bool {
 | 
			
		||||
	mimetype := mime.TypeByExtension(extension)
 | 
			
		||||
	if strings.HasPrefix(mimetype, matchType) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	switch matchType {
 | 
			
		||||
	case "doc":
 | 
			
		||||
		return isDoc(extension)
 | 
			
		||||
	case "archive":
 | 
			
		||||
		return isArchive(extension)
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isDoc(extension string) bool {
 | 
			
		||||
	for _, typefile := range documentTypes {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isArchive(extension string) bool {
 | 
			
		||||
	for _, typefile := range compressedFile {
 | 
			
		||||
		if extension == typefile {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,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)
 | 
			
		||||
	}
 | 
			
		||||
	GlobalConfiguration.UserDefaults.Perm = GlobalConfiguration.UserDefaults.Permissions
 | 
			
		||||
	GlobalConfiguration.Server.Root = "/srv" // hardcoded for now. TODO allow changing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadConfigFile(configFile string) []byte {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +25,7 @@ func loadConfigFile(configFile string) []byte {
 | 
			
		|||
	yamlFile, err := os.Open(configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("ERROR: opening config file\n %v\n WARNING: Using default config only\n If this was a mistake, please make sure the file exists and is accessible by the filebrowser binary.\n\n", err)
 | 
			
		||||
		setDefaults()
 | 
			
		||||
		GlobalConfiguration = setDefaults()
 | 
			
		||||
		return []byte{}
 | 
			
		||||
	}
 | 
			
		||||
	defer yamlFile.Close()
 | 
			
		||||
| 
						 | 
				
			
			@ -62,14 +61,18 @@ func setDefaults() Settings {
 | 
			
		|||
		},
 | 
			
		||||
		Auth: Auth{
 | 
			
		||||
			Method: "password",
 | 
			
		||||
			Signup: true,
 | 
			
		||||
			Recaptcha: Recaptcha{
 | 
			
		||||
				Host: "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		UserDefaults: UserDefaults{
 | 
			
		||||
			Scope:        ".",
 | 
			
		||||
			LockPassword: false,
 | 
			
		||||
			HideDotfiles: true,
 | 
			
		||||
			Scope:           ".",
 | 
			
		||||
			LockPassword:    false,
 | 
			
		||||
			HideDotfiles:    true,
 | 
			
		||||
			DarkMode:        false,
 | 
			
		||||
			DisableSettings: false,
 | 
			
		||||
			Locale:          "en",
 | 
			
		||||
			Permissions: users.Permissions{
 | 
			
		||||
				Create:   true,
 | 
			
		||||
				Rename:   true,
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +80,23 @@ func setDefaults() Settings {
 | 
			
		|||
				Delete:   true,
 | 
			
		||||
				Share:    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 (
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StorageBackend is a settings storage backend.
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +58,7 @@ func (s *Storage) Save(set *Settings) error {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if set.UserDefaults.ViewMode == "" {
 | 
			
		||||
		set.UserDefaults.ViewMode = users.MosaicViewMode
 | 
			
		||||
		set.UserDefaults.ViewMode = "normal"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if set.Rules == nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,19 +5,6 @@ import (
 | 
			
		|||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Apply applies the default options to a user.
 | 
			
		||||
func (d *UserDefaults) Apply(u *users.User) {
 | 
			
		||||
	u.Scope = d.Scope
 | 
			
		||||
	u.Locale = d.Locale
 | 
			
		||||
	u.ViewMode = d.ViewMode
 | 
			
		||||
	u.SingleClick = d.SingleClick
 | 
			
		||||
	u.Perm = d.Perm
 | 
			
		||||
	u.Sorting = d.Sorting
 | 
			
		||||
	u.Commands = d.Commands
 | 
			
		||||
	u.HideDotfiles = d.HideDotfiles
 | 
			
		||||
	u.DateFormat = d.DateFormat
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Settings struct {
 | 
			
		||||
	Key              []byte              `json:"key"`
 | 
			
		||||
	Signup           bool                `json:"signup"`
 | 
			
		||||
| 
						 | 
				
			
			@ -74,13 +61,13 @@ type Frontend struct {
 | 
			
		|||
	DisableExternal       bool   `json:"disableExternal"`
 | 
			
		||||
	DisableUsedPercentage bool   `json:"disableUsedPercentage"`
 | 
			
		||||
	Files                 string `json:"files"`
 | 
			
		||||
	Theme                 string `json:"theme"`
 | 
			
		||||
	Color                 string `json:"color"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserDefaults is a type that holds the default values
 | 
			
		||||
// for some fields on User.
 | 
			
		||||
type UserDefaults struct {
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
	LockPassword bool         `json:"lockPassword"`
 | 
			
		||||
	Scope        string       `json:"scope"`
 | 
			
		||||
	Locale       string       `json:"locale"`
 | 
			
		||||
| 
						 | 
				
			
			@ -88,12 +75,27 @@ type UserDefaults struct {
 | 
			
		|||
	SingleClick  bool         `json:"singleClick"`
 | 
			
		||||
	Rules        []rules.Rule `json:"rules"`
 | 
			
		||||
	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"`
 | 
			
		||||
		Asc bool   `json:"asc"`
 | 
			
		||||
	} `json:"sorting"`
 | 
			
		||||
	Perm         users.Permissions `json:"perm"`
 | 
			
		||||
	Permissions  users.Permissions `json:"permissions"`
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
	Commands     []string          `json:"commands,omitemptys"`
 | 
			
		||||
=======
 | 
			
		||||
	Commands     []string          `json:"commands,omitempty"`
 | 
			
		||||
>>>>>>> v0.2.1
 | 
			
		||||
	HideDotfiles bool              `json:"hideDotfiles"`
 | 
			
		||||
	DateFormat   bool              `json:"dateFormat"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,12 +28,13 @@ frontend:
 | 
			
		|||
  disableExternal: true
 | 
			
		||||
  disableUsedPercentage: true
 | 
			
		||||
  files: ""
 | 
			
		||||
  theme: ""
 | 
			
		||||
  color: ""
 | 
			
		||||
userDefaults:
 | 
			
		||||
  scope: ""
 | 
			
		||||
  locale: ""
 | 
			
		||||
  viewMode: ""
 | 
			
		||||
  darkMode: true
 | 
			
		||||
  disableSettings: false
 | 
			
		||||
  singleClick: true
 | 
			
		||||
  sorting:
 | 
			
		||||
    by: ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package bolt
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"reflect"
 | 
			
		||||
 | 
			
		||||
	"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 {
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
	password, err := users.HashPwd(user.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
	if err == storm.ErrAlreadyExists {
 | 
			
		||||
		return errors.ErrExist
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,14 @@
 | 
			
		|||
package users
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HashPwd hashes a password.
 | 
			
		||||
func HashPwd(password string) (string, error) {
 | 
			
		||||
	log.Println("hashing password", password)
 | 
			
		||||
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
			
		||||
	return string(bytes), err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
 | 
			
		|||
 | 
			
		||||
// Update updates a user in the database.
 | 
			
		||||
func (s *Storage) Update(user *User, fields ...string) error {
 | 
			
		||||
	err := user.Clean("", fields...)
 | 
			
		||||
	err := user.Clean("")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,16 +6,10 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/errors"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/files"
 | 
			
		||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ListViewMode   = "list"
 | 
			
		||||
	MosaicViewMode = "mosaic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Permissions struct {
 | 
			
		||||
	Admin    bool `json:"admin"`
 | 
			
		||||
	Execute  bool `json:"execute"`
 | 
			
		||||
| 
						 | 
				
			
			@ -29,21 +23,23 @@ type Permissions struct {
 | 
			
		|||
 | 
			
		||||
// User describes a user.
 | 
			
		||||
type User struct {
 | 
			
		||||
	ID           uint          `storm:"id,increment" json:"id"`
 | 
			
		||||
	Username     string        `storm:"unique" json:"username"`
 | 
			
		||||
	Password     string        `json:"password"`
 | 
			
		||||
	Scope        string        `json:"scope"`
 | 
			
		||||
	Locale       string        `json:"locale"`
 | 
			
		||||
	LockPassword bool          `json:"lockPassword"`
 | 
			
		||||
	ViewMode     string        `json:"viewMode"`
 | 
			
		||||
	SingleClick  bool          `json:"singleClick"`
 | 
			
		||||
	Perm         Permissions   `json:"perm"`
 | 
			
		||||
	Commands     []string      `json:"commands"`
 | 
			
		||||
	Sorting      files.Sorting `json:"sorting"`
 | 
			
		||||
	Fs           afero.Fs      `json:"-" yaml:"-"`
 | 
			
		||||
	Rules        []rules.Rule  `json:"rules"`
 | 
			
		||||
	HideDotfiles bool          `json:"hideDotfiles"`
 | 
			
		||||
	DateFormat   bool          `json:"dateFormat"`
 | 
			
		||||
	DarkMode        bool          `json:"darkMode"`
 | 
			
		||||
	DisableSettings bool          `json:"disableSettings"`
 | 
			
		||||
	ID              uint          `storm:"id,increment" json:"id"`
 | 
			
		||||
	Username        string        `storm:"unique" json:"username"`
 | 
			
		||||
	Password        string        `json:"password"`
 | 
			
		||||
	Scope           string        `json:"scope"`
 | 
			
		||||
	Locale          string        `json:"locale"`
 | 
			
		||||
	LockPassword    bool          `json:"lockPassword"`
 | 
			
		||||
	ViewMode        string        `json:"viewMode"`
 | 
			
		||||
	SingleClick     bool          `json:"singleClick"`
 | 
			
		||||
	Perm            Permissions   `json:"perm"`
 | 
			
		||||
	Commands        []string      `json:"commands"`
 | 
			
		||||
	Sorting         files.Sorting `json:"sorting"`
 | 
			
		||||
	Fs              afero.Fs      `json:"-" yaml:"-"`
 | 
			
		||||
	Rules           []rules.Rule  `json:"rules"`
 | 
			
		||||
	HideDotfiles    bool          `json:"hideDotfiles"`
 | 
			
		||||
	DateFormat      bool          `json:"dateFormat"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRules implements rules.Provider.
 | 
			
		||||
| 
						 | 
				
			
			@ -51,53 +47,11 @@ func (u *User) GetRules() []rules.Rule {
 | 
			
		|||
	return u.Rules
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var checkableFields = []string{
 | 
			
		||||
	"Username",
 | 
			
		||||
	"Password",
 | 
			
		||||
	"Scope",
 | 
			
		||||
	"ViewMode",
 | 
			
		||||
	"Commands",
 | 
			
		||||
	"Sorting",
 | 
			
		||||
	"Rules",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clean cleans up a user and verifies if all its fields
 | 
			
		||||
// are alright to be saved.
 | 
			
		||||
//
 | 
			
		||||
//nolint:gocyclo
 | 
			
		||||
func (u *User) Clean(baseScope string, fields ...string) error {
 | 
			
		||||
	if len(fields) == 0 {
 | 
			
		||||
		fields = checkableFields
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, field := range fields {
 | 
			
		||||
		switch field {
 | 
			
		||||
		case "Username":
 | 
			
		||||
			if u.Username == "" {
 | 
			
		||||
				return errors.ErrEmptyUsername
 | 
			
		||||
			}
 | 
			
		||||
		case "Password":
 | 
			
		||||
			if u.Password == "" {
 | 
			
		||||
				return errors.ErrEmptyPassword
 | 
			
		||||
			}
 | 
			
		||||
		case "ViewMode":
 | 
			
		||||
			if u.ViewMode == "" {
 | 
			
		||||
				u.ViewMode = ListViewMode
 | 
			
		||||
			}
 | 
			
		||||
		case "Commands":
 | 
			
		||||
			if u.Commands == nil {
 | 
			
		||||
				u.Commands = []string{}
 | 
			
		||||
			}
 | 
			
		||||
		case "Sorting":
 | 
			
		||||
			if u.Sorting.By == "" {
 | 
			
		||||
				u.Sorting.By = "name"
 | 
			
		||||
			}
 | 
			
		||||
		case "Rules":
 | 
			
		||||
			if u.Rules == nil {
 | 
			
		||||
				u.Rules = []rules.Rule{}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
func (u *User) Clean(baseScope string) error {
 | 
			
		||||
 | 
			
		||||
	if u.Fs == nil {
 | 
			
		||||
		scope := u.Scope
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ package version
 | 
			
		|||
 | 
			
		||||
var (
 | 
			
		||||
	// Version is the current File Browser version.
 | 
			
		||||
	Version = "(0.2.0)"
 | 
			
		||||
	Version = "(0.2.1)"
 | 
			
		||||
	// CommitSHA is the commmit sha.
 | 
			
		||||
	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:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
signup: false
 | 
			
		||||
adminUsername: admin
 | 
			
		||||
adminPassword: admin
 | 
			
		||||
server:
 | 
			
		||||
  indexingInterval: 5
 | 
			
		||||
  numImageProcessors: 4
 | 
			
		||||
| 
						 | 
				
			
			@ -30,18 +33,18 @@ auth:
 | 
			
		|||
  header: ""
 | 
			
		||||
  method: json
 | 
			
		||||
  command: ""
 | 
			
		||||
  signup: false
 | 
			
		||||
  shell: ""
 | 
			
		||||
frontend:
 | 
			
		||||
  name: ""
 | 
			
		||||
  disableExternal: false
 | 
			
		||||
  disableUsedPercentage: true
 | 
			
		||||
  files: ""
 | 
			
		||||
  theme: ""
 | 
			
		||||
  color: ""
 | 
			
		||||
userDefaults:
 | 
			
		||||
  settingsAllowed: true
 | 
			
		||||
  darkMode: false
 | 
			
		||||
  scope: ""
 | 
			
		||||
  locale: ""
 | 
			
		||||
  locale: "en"
 | 
			
		||||
  viewMode: ""
 | 
			
		||||
  singleClick: true
 | 
			
		||||
  sorting:
 | 
			
		||||
| 
						 | 
				
			
			@ -64,34 +67,38 @@ userDefaults:
 | 
			
		|||
Here are the defaults if nothing is set:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
Signup: true
 | 
			
		||||
AdminUsername: admin
 | 
			
		||||
AdminPassword: admin
 | 
			
		||||
Server:
 | 
			
		||||
  EnableThumbnails: true
 | 
			
		||||
  EnableExec: false
 | 
			
		||||
  IndexingInterval: 5
 | 
			
		||||
  Port: 8080
 | 
			
		||||
  NumImageProcessors: 4
 | 
			
		||||
  BaseURL: ""
 | 
			
		||||
  Database: database.db
 | 
			
		||||
  Log: stdout
 | 
			
		||||
  Root: /srv
 | 
			
		||||
Auth:
 | 
			
		||||
  Method: password
 | 
			
		||||
  Recaptcha:
 | 
			
		||||
    Host: ""
 | 
			
		||||
UserDefaults:
 | 
			
		||||
  Scope: "."
 | 
			
		||||
  LockPassword: false
 | 
			
		||||
  HideDotfiles: true
 | 
			
		||||
  Permissions:
 | 
			
		||||
    Create: true
 | 
			
		||||
    Rename: true
 | 
			
		||||
    Modify: true
 | 
			
		||||
    Delete: true
 | 
			
		||||
    Share: true
 | 
			
		||||
    Download: true
 | 
			
		||||
signup: true
 | 
			
		||||
adminUsername: admin
 | 
			
		||||
adminPassword: admin
 | 
			
		||||
server:
 | 
			
		||||
  enableThumbnails: true
 | 
			
		||||
  enableExec: false
 | 
			
		||||
  indexingInterval: 5
 | 
			
		||||
  port: 8080
 | 
			
		||||
  numImageProcessors: 4
 | 
			
		||||
  baseURL: ""
 | 
			
		||||
  database: database.db
 | 
			
		||||
  log: stdout
 | 
			
		||||
  root: /srv
 | 
			
		||||
auth:
 | 
			
		||||
  method: password
 | 
			
		||||
  recaptcha:
 | 
			
		||||
    host: ""
 | 
			
		||||
userDefaults:
 | 
			
		||||
  settingsAllowed: true
 | 
			
		||||
  darkMode: false
 | 
			
		||||
  scope: ""
 | 
			
		||||
  locale: "en"
 | 
			
		||||
  scope: "."
 | 
			
		||||
  lockPassword: false
 | 
			
		||||
  hideDotfiles: true
 | 
			
		||||
  permissions:
 | 
			
		||||
    create: true
 | 
			
		||||
    rename: true
 | 
			
		||||
    modify: true
 | 
			
		||||
    delete: true
 | 
			
		||||
    share: true
 | 
			
		||||
    download: true
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## About each configuration
 | 
			
		||||
| 
						 | 
				
			
			@ -100,17 +107,17 @@ UserDefaults:
 | 
			
		|||
 | 
			
		||||
## 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
 | 
			
		||||
 | 
			
		||||
- `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.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -118,23 +125,23 @@ UserDefaults:
 | 
			
		|||
 | 
			
		||||
- `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.
 | 
			
		||||
 | 
			
		||||
- `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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -149,17 +156,16 @@ UserDefaults:
 | 
			
		|||
- `header`: This is the authentication header.
 | 
			
		||||
 | 
			
		||||
- `method`: This is the authentication method used (e.g., "json"). Possible values:
 | 
			
		||||
  - password - username and password
 | 
			
		||||
  - hook - hook authentication
 | 
			
		||||
  - proxy - proxy authentication
 | 
			
		||||
  - oath - oath authentication
 | 
			
		||||
  - `password` - username and password
 | 
			
		||||
  - `hook` - hook authentication
 | 
			
		||||
  - `proxy` - proxy authentication
 | 
			
		||||
  - `oath` - oath authentication
 | 
			
		||||
  - `noauth` - no authentication/login required.
 | 
			
		||||
 | 
			
		||||
- `command`: This is the authentication command.
 | 
			
		||||
 | 
			
		||||
- `signup`: This boolean value indicates whether user signup is enabled.
 | 
			
		||||
- `command`: Deprecated: This is the authentication command.
 | 
			
		||||
 | 
			
		||||
- `shell`: This is the shell configuration.
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
### Frontend configuration settings
 | 
			
		||||
 | 
			
		||||
- `name`: This is the name of the frontend.
 | 
			
		||||
| 
						 | 
				
			
			@ -173,22 +179,26 @@ UserDefaults:
 | 
			
		|||
- `theme`: This is the theme configuration.
 | 
			
		||||
 | 
			
		||||
- `color`: This is the color configuration.
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
### UserDefaults configuration settings
 | 
			
		||||
 | 
			
		||||
- `darkMode`: Determines whether dark mode is enabled for the user (`true` or `false`)
 | 
			
		||||
 | 
			
		||||
- `settingsAllowed`: Determines whether settings page is enabled for the user (`true` or `false`)
 | 
			
		||||
 | 
			
		||||
- `scope`: This is a scope of the permissions, "." or "./" means all directories, "./downloads" would mean only the downloads folder.
 | 
			
		||||
 | 
			
		||||
- `locale`: 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`:
 | 
			
		||||
 | 
			
		||||
  - `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`:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -208,9 +218,8 @@ UserDefaults:
 | 
			
		|||
 | 
			
		||||
  - `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.
 | 
			
		||||
 | 
			
		||||
- `dateFormat`: This boolean value determines whether date formatting is enabled.
 | 
			
		||||
- `hideDotfiles`: This boolean value determines whether dotfiles are hidden. (`true` or `false`)
 | 
			
		||||
 | 
			
		||||
- `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": {
 | 
			
		||||
    "@vue/cli-service": "^5.0.8",
 | 
			
		||||
    "compression-webpack-plugin": "^10.0.0",
 | 
			
		||||
    "eslint": "^8.51.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.17.0",
 | 
			
		||||
    "vue-template-compiler": "^2.6.10"
 | 
			
		||||
  },
 | 
			
		||||
  "postcss": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,8 +128,8 @@
 | 
			
		|||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  [{[ if .Theme -]}]
 | 
			
		||||
    <link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
 | 
			
		||||
  [{[ if .darkMode -]}]
 | 
			
		||||
    <link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/dark.css" />
 | 
			
		||||
  [{[ end ]}]
 | 
			
		||||
  [{[ if .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>
 | 
			
		||||
  <router-view></router-view>
 | 
			
		||||
    <router-view></router-view>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "app",
 | 
			
		||||
  computed: {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const loading = document.getElementById("loading");
 | 
			
		||||
    loading.classList.add("done");
 | 
			
		||||
 | 
			
		||||
    setTimeout(function () {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      loading.parentNode.removeChild(loading);
 | 
			
		||||
    }, 200);
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -20,5 +22,7 @@ export default {
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
/* Always load styles.css */
 | 
			
		||||
@import "./css/styles.css";
 | 
			
		||||
@import "./css/dark.css";
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<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">
 | 
			
		||||
      <button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
 | 
			
		||||
        <i class="material-icons">close</i>
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +149,7 @@
 | 
			
		|||
  padding-bottom: 1em;
 | 
			
		||||
  -webkit-transition: width 0.3s ease 0s;
 | 
			
		||||
  transition: width 0.3s ease 0s;
 | 
			
		||||
  background-color: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#result-desktop {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,6 +173,8 @@
 | 
			
		|||
  background-color: lightgray;
 | 
			
		||||
  max-height: 80vh;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#search.active #result-desktop ul li a {
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +204,7 @@
 | 
			
		|||
 | 
			
		||||
/* Search */
 | 
			
		||||
#search {
 | 
			
		||||
  background-color:unset;
 | 
			
		||||
  z-index:3;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: .5em;
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +318,7 @@ body.rtl #search #result ul>* {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
#search.active #input {
 | 
			
		||||
  background-color: lightgray;
 | 
			
		||||
  background-color: var(--background);
 | 
			
		||||
  border-color: black;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  border-bottom-style: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -485,7 +489,7 @@ export default {
 | 
			
		|||
        { label: "Photos", value: "type:image" },
 | 
			
		||||
        { label: "Audio", value: "type:audio" },
 | 
			
		||||
        { label: "Videos", value: "type:video" },
 | 
			
		||||
        { label: "Documents", value: "type:docs" },
 | 
			
		||||
        { label: "Documents", value: "type:doc" },
 | 
			
		||||
        { label: "Archives", value: "type:archive" },
 | 
			
		||||
      ],
 | 
			
		||||
      value: "",
 | 
			
		||||
| 
						 | 
				
			
			@ -538,6 +542,9 @@ export default {
 | 
			
		|||
  computed: {
 | 
			
		||||
    ...mapState(["user", "show"]),
 | 
			
		||||
    ...mapGetters(["isListing"]),
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    showBoxes() {
 | 
			
		||||
      return this.searchTypes == "";
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <nav :class="{ active }">
 | 
			
		||||
  <nav :class="{ active, 'dark-mode': isDarkMode }">
 | 
			
		||||
    <template v-if="isLogged">
 | 
			
		||||
      <button class="action" @click="toRoot" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
 | 
			
		||||
        <i class="material-icons">folder</i>
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +87,9 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["user"]),
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    ...mapGetters(["isLogged"]),
 | 
			
		||||
    active() {
 | 
			
		||||
      return this.$store.state.show === "sidebar";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
    window.addEventListener("resize", this.onResize);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("resize", this.onResize);
 | 
			
		||||
    document.removeEventListener("mouseup", this.onMouseUp);
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,11 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import { logoURL } from "@/utils/constants";
 | 
			
		||||
import Action from "@/components/header/Action.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "header-bar",
 | 
			
		||||
  props: ["showLogo", "showMenu"],
 | 
			
		||||
  components: {
 | 
			
		||||
    Action,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {
 | 
			
		||||
      logoURL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -178,7 +178,7 @@ export default {
 | 
			
		|||
      this.$showSuccess(this.$t("success.linkCopied"));
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.clip.destroy();
 | 
			
		||||
  },
 | 
			
		||||
  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;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  padding: .5em 1em;
 | 
			
		||||
  border-radius: .1em;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  background: var(--blue);
 | 
			
		||||
  color: white;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
.input {
 | 
			
		||||
  border-radius: .1em;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
  padding: .5em 1em;
 | 
			
		||||
  background: white;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
.share__box {
 | 
			
		||||
  box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  border-radius: 0.2em;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
  margin: 5px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -203,6 +203,7 @@ body.rtl .breadcrumbs a {
 | 
			
		|||
  width: 95%;
 | 
			
		||||
  max-width: 30em;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.floating {
 | 
			
		||||
| 
						 | 
				
			
			@ -459,7 +460,7 @@ body.rtl .card .card-title>*:first-child {
 | 
			
		|||
.card .card-action.full .action {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  padding: 2em;
 | 
			
		||||
  border-radius: 0.2em;
 | 
			
		||||
  border-radius: 1em;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,41 +85,53 @@ body.rtl #listing {
 | 
			
		|||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic {
 | 
			
		||||
#listing {
 | 
			
		||||
  padding-top: 1em;
 | 
			
		||||
  margin: 0 -0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item {
 | 
			
		||||
#listing.gallery .item,
 | 
			
		||||
#listing.compact .item,
 | 
			
		||||
#listing.normal .item,
 | 
			
		||||
#listing.list .item {
 | 
			
		||||
  width: calc(33% - 1em);
 | 
			
		||||
  max-width: 300px;
 | 
			
		||||
  margin: .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);
 | 
			
		||||
}
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .header {
 | 
			
		||||
#listing .header {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item div:first-of-type {
 | 
			
		||||
#listing .item div:first-of-type {
 | 
			
		||||
  width: 5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic .item div:last-of-type {
 | 
			
		||||
#listing .item div:last-of-type {
 | 
			
		||||
  width: calc(100% - 5vw);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item div:first-of-type {
 | 
			
		||||
#listing.gallery .item div:first-of-type {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 12em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item div:last-of-type {
 | 
			
		||||
#listing.gallery .item div:last-of-type {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: 0.5em;
 | 
			
		||||
  padding: 1em;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,19 +139,19 @@ body.rtl #listing {
 | 
			
		|||
  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;
 | 
			
		||||
  background: linear-gradient(#0000, #0009);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item i {
 | 
			
		||||
#listing.gallery .item i {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
    font-size: 8em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.mosaic.gallery .item img {
 | 
			
		||||
#listing.gallery .item img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +161,109 @@ body.rtl #listing {
 | 
			
		|||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  border-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact h2 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type {
 | 
			
		||||
  width: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type i {
 | 
			
		||||
  font-size: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:first-of-type img {
 | 
			
		||||
  width: 2em;
 | 
			
		||||
  height: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item div:last-of-type {
 | 
			
		||||
  width: calc(100% - 3em);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item .name {
 | 
			
		||||
  width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .item .size {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header i {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  margin-left: .2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header {
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  padding: .85em;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .name {
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header span {
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header i {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: .1s ease all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header p:hover i,
 | 
			
		||||
#listing.compact .header .active i {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.compact .header .active {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -160,14 +275,10 @@ body.rtl #listing {
 | 
			
		|||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  padding: .5em;
 | 
			
		||||
  border-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list h2 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing .item[aria-selected=true] {
 | 
			
		||||
  background: var(--blue) !important;
 | 
			
		||||
  color: var(--item-selected) !important;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +311,7 @@ body.rtl #listing {
 | 
			
		|||
  width: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing .item.header {
 | 
			
		||||
#listing .header {
 | 
			
		||||
  display: none !important;
 | 
			
		||||
  background-color: #ccc;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -211,20 +322,34 @@ body.rtl #listing {
 | 
			
		|||
  margin-left: .2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header {
 | 
			
		||||
#listing.compact .header,
 | 
			
		||||
#listing.list .header {
 | 
			
		||||
  display: flex !important;
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
  background: var(--surfacePrimary);
 | 
			
		||||
  border-top-left-radius: 1em;
 | 
			
		||||
  border-top-right-radius: 1em;
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  padding: .85em;
 | 
			
		||||
  width:100%;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
#listing.list .item:first-child {
 | 
			
		||||
  margin-top: .5em;
 | 
			
		||||
  border-top-left-radius: 1em;
 | 
			
		||||
  border-top-right-radius: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .name {
 | 
			
		||||
#listing.list .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +357,7 @@ body.rtl #listing {
 | 
			
		|||
  color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header>div:first-child {
 | 
			
		||||
#listing.list .header>div:first-child {
 | 
			
		||||
  width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +365,7 @@ body.rtl #listing {
 | 
			
		|||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .name {
 | 
			
		||||
#listing.list .header .name {
 | 
			
		||||
  margin-right: 3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +383,7 @@ body.rtl #listing {
 | 
			
		|||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#listing.list .item.header .active {
 | 
			
		||||
#listing.list .header .active {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,14 +7,14 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 800px) {
 | 
			
		||||
  #listing.list .item div:last-of-type{
 | 
			
		||||
    display:block;
 | 
			
		||||
    width:100%;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    padding-bottom: 5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .size {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .name {
 | 
			
		||||
    width: 60%;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,9 @@
 | 
			
		|||
  #listing {
 | 
			
		||||
    margin-bottom: 5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing .item {
 | 
			
		||||
    min-width: 100%
 | 
			
		||||
  }
 | 
			
		||||
  body.rtl #listing {
 | 
			
		||||
    margin-right: unset;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +119,6 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@media (max-width: 450px) {
 | 
			
		||||
  #listing.list .item .modified {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #listing.list .item .name {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,7 @@ export function parseToken(token) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  const data = JSON.parse(Base64.decode(parts[1]));
 | 
			
		||||
 | 
			
		||||
  document.cookie = `auth=${token}; path=/`;
 | 
			
		||||
 | 
			
		||||
  localStorage.setItem("jwt", token);
 | 
			
		||||
  store.commit("setJWT", token);
 | 
			
		||||
  store.commit("setSession", generateRandomCode(8));
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +38,6 @@ export async function login(username, password, recaptcha) {
 | 
			
		|||
    },
 | 
			
		||||
    body: JSON.stringify(data),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const body = await res.text();
 | 
			
		||||
 | 
			
		||||
  if (res.status === 200) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,6 @@ const logoURL = `${staticURL}/img/logo.svg`;
 | 
			
		|||
const noAuth = window.FileBrowser.NoAuth;
 | 
			
		||||
const authMethod = window.FileBrowser.AuthMethod;
 | 
			
		||||
const loginPage = window.FileBrowser.LoginPage;
 | 
			
		||||
const theme = window.FileBrowser.Theme;
 | 
			
		||||
const enableThumbs = window.FileBrowser.EnableThumbs;
 | 
			
		||||
const resizePreview = window.FileBrowser.ResizePreview;
 | 
			
		||||
const enableExec = window.FileBrowser.EnableExec;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +29,6 @@ export {
 | 
			
		|||
  noAuth,
 | 
			
		||||
  authMethod,
 | 
			
		||||
  loginPage,
 | 
			
		||||
  theme,
 | 
			
		||||
  enableThumbs,
 | 
			
		||||
  resizePreview,
 | 
			
		||||
  enableExec,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,6 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar";
 | 
			
		||||
 | 
			
		||||
const errors = {
 | 
			
		||||
  0: {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +31,6 @@ const errors = {
 | 
			
		|||
export default {
 | 
			
		||||
  name: "errors",
 | 
			
		||||
  components: {
 | 
			
		||||
    HeaderBar,
 | 
			
		||||
  },
 | 
			
		||||
  props: ["errorCode", "showHeader"],
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -80,10 +80,10 @@ export default {
 | 
			
		|||
  mounted() {
 | 
			
		||||
    window.addEventListener("keydown", this.keyEvent);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
  },
 | 
			
		||||
  destroyed() {
 | 
			
		||||
  unmounted() {
 | 
			
		||||
    if (this.$store.state.showShell) {
 | 
			
		||||
      this.$store.commit("toggleShell");
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +91,7 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  currentView(newView) {
 | 
			
		||||
    // Commit the new value to the store
 | 
			
		||||
    this.setCurrentValue(this.newValue);
 | 
			
		||||
    this.setCurrentValue(newView);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapMutations(["setLoading","setCurrentView"]),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,10 @@
 | 
			
		|||
    <editorBar v-else-if="currentView === 'editor'"></editorBar>
 | 
			
		||||
    <defaultBar v-else></defaultBar>
 | 
			
		||||
    <sidebar></sidebar>
 | 
			
		||||
    <main>
 | 
			
		||||
    <main :class="{ 'dark-mode': isDarkMode }">
 | 
			
		||||
      <router-view></router-view>
 | 
			
		||||
    </main>
 | 
			
		||||
    <prompts></prompts>
 | 
			
		||||
    <prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
 | 
			
		||||
    <upload-files></upload-files>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,6 @@ import editorBar from "./bars/EditorBar.vue"
 | 
			
		|||
import defaultBar from "./bars/Default.vue"
 | 
			
		||||
import listingBar from "./bars/ListingBar.vue"
 | 
			
		||||
import Prompts from "@/components/prompts/Prompts";
 | 
			
		||||
import Action from "@/components/header/Action";
 | 
			
		||||
import { mapState, mapGetters } from "vuex";
 | 
			
		||||
import Sidebar from "@/components/Sidebar.vue";
 | 
			
		||||
import UploadFiles from "../components/prompts/UploadFiles";
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +30,6 @@ export default {
 | 
			
		|||
    defaultBar,
 | 
			
		||||
    editorBar,
 | 
			
		||||
    listingBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    Sidebar,
 | 
			
		||||
    Prompts,
 | 
			
		||||
    UploadFiles,
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +45,9 @@ export default {
 | 
			
		|||
  computed: {
 | 
			
		||||
    ...mapGetters(["isLogged", "progress", "isListing"]),
 | 
			
		||||
    ...mapState(["req", "user", "state"]),
 | 
			
		||||
 | 
			
		||||
    isDarkMode() {
 | 
			
		||||
      return this.user.darkMode === true
 | 
			
		||||
    },
 | 
			
		||||
    isExecEnabled: () => enableExec,
 | 
			
		||||
    currentView() {
 | 
			
		||||
      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>
 | 
			
		||||
  <div class="dashboard">
 | 
			
		||||
    <div id="nav">
 | 
			
		||||
      <div class="wrapper">
 | 
			
		||||
      <div v-if="settingsEnabled" class="wrapper">
 | 
			
		||||
        <ul>
 | 
			
		||||
          <router-link to="/settings/profile"
 | 
			
		||||
            ><li :class="{ active: $route.path === '/settings/profile' }">
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +57,10 @@ export default {
 | 
			
		|||
    this.$store.commit("updateRequest", { name: "Settings" });
 | 
			
		||||
  },
 | 
			
		||||
  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 moment from "moment";
 | 
			
		||||
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar";
 | 
			
		||||
import Action from "@/components/header/Action";
 | 
			
		||||
import Breadcrumbs from "@/components/Breadcrumbs";
 | 
			
		||||
import Errors from "@/views/Errors";
 | 
			
		||||
import QrcodeVue from "qrcode.vue";
 | 
			
		||||
| 
						 | 
				
			
			@ -171,8 +169,6 @@ import Clipboard from "clipboard";
 | 
			
		|||
export default {
 | 
			
		||||
  name: "share",
 | 
			
		||||
  components: {
 | 
			
		||||
    HeaderBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    Breadcrumbs,
 | 
			
		||||
    Item,
 | 
			
		||||
    QrcodeVue,
 | 
			
		||||
| 
						 | 
				
			
			@ -206,7 +202,7 @@ export default {
 | 
			
		|||
      this.$showSuccess(this.$t("success.linkCopied"));
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    this.clip.destroy();
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,24 +18,18 @@
 | 
			
		|||
import Vue from "vue";
 | 
			
		||||
import { mapState, mapGetters, mapMutations } from "vuex";
 | 
			
		||||
import { users, files as api } from "@/api";
 | 
			
		||||
import { enableExec } from "@/utils/constants";
 | 
			
		||||
import url from "@/utils/url";
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar.vue";
 | 
			
		||||
import Action from "@/components/header/Action.vue";
 | 
			
		||||
import * as upload from "@/utils/upload";
 | 
			
		||||
import css from "@/utils/css";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import Search from "@/components/Search.vue";
 | 
			
		||||
 | 
			
		||||
import Item from "@/components/files/ListingItem.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "listing",
 | 
			
		||||
  components: {
 | 
			
		||||
    HeaderBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    Search,
 | 
			
		||||
    Item,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +38,7 @@ export default {
 | 
			
		|||
      dragCounter: 0,
 | 
			
		||||
      width: window.innerWidth,
 | 
			
		||||
      itemWeight: 0,
 | 
			
		||||
      viewModes: ['list', 'compact', 'normal', 'gallery'],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -112,8 +107,9 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +162,7 @@ export default {
 | 
			
		|||
    document.addEventListener("dragleave", this.dragLeave);
 | 
			
		||||
    document.addEventListener("drop", this.drop);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    // Remove event listeners before destroying this page.
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    window.removeEventListener("scroll", this.scrollEvent);
 | 
			
		||||
| 
						 | 
				
			
			@ -265,21 +261,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    switchView: async function () {
 | 
			
		||||
      this.$store.commit("closeHovers");
 | 
			
		||||
      const modes = {
 | 
			
		||||
        list: "mosaic",
 | 
			
		||||
        mosaic: "mosaic gallery",
 | 
			
		||||
        "mosaic gallery": "list",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const currentIndex = this.viewModes.indexOf(this.user.viewMode);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
			
		||||
      const data = {
 | 
			
		||||
        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.setItemWeight();
 | 
			
		||||
      //this.fillWindow();
 | 
			
		||||
    },
 | 
			
		||||
    preventDefault(event) {
 | 
			
		||||
      // Wrapper around prevent default.
 | 
			
		||||
| 
						 | 
				
			
			@ -381,7 +370,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        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;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -589,7 +578,6 @@ export default {
 | 
			
		|||
      }
 | 
			
		||||
      this.$store.commit("updateRequest", {});
 | 
			
		||||
      let uri = url.removeLastDir(this.$route.path) + "/";
 | 
			
		||||
      console.log(url)
 | 
			
		||||
      this.$router.push({ path: uri });
 | 
			
		||||
    },
 | 
			
		||||
    upload: function () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,14 +30,12 @@ import url from "@/utils/url";
 | 
			
		|||
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar";
 | 
			
		||||
import Action from "@/components/header/Action";
 | 
			
		||||
import Breadcrumbs from "@/components/Breadcrumbs";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "editor",
 | 
			
		||||
  components: {
 | 
			
		||||
    HeaderBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    Breadcrumbs,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {};
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +75,7 @@ export default {
 | 
			
		|||
  created() {
 | 
			
		||||
    window.addEventListener("keydown", this.keyEvent);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    this.editor.destroy();
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,13 +30,11 @@ import { mapState, mapGetters, mapMutations } from "vuex";
 | 
			
		|||
import { users, files as api } from "@/api";
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar.vue";
 | 
			
		||||
import Action from "@/components/header/Action.vue";
 | 
			
		||||
import url from "@/utils/url";
 | 
			
		||||
import * as upload from "@/utils/upload";
 | 
			
		||||
import css from "@/utils/css";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import Search from "@/components/Search.vue";
 | 
			
		||||
 | 
			
		||||
import Item from "@/components/files/ListingItem.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "listing",
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +42,6 @@ export default {
 | 
			
		|||
    HeaderBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    Search,
 | 
			
		||||
    Item,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +50,7 @@ export default {
 | 
			
		|||
      dragCounter: 0,
 | 
			
		||||
      width: window.innerWidth,
 | 
			
		||||
      itemWeight: 0,
 | 
			
		||||
      viewModes: ['list', 'compact', 'normal', 'gallery'],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -118,8 +116,9 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -172,7 +171,7 @@ export default {
 | 
			
		|||
    document.addEventListener("dragleave", this.dragLeave);
 | 
			
		||||
    document.addEventListener("drop", this.drop);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    // Remove event listeners before destroying this page.
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    window.removeEventListener("scroll", this.scrollEvent);
 | 
			
		||||
| 
						 | 
				
			
			@ -271,21 +270,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    switchView: async function () {
 | 
			
		||||
      this.$store.commit("closeHovers");
 | 
			
		||||
      const modes = {
 | 
			
		||||
        list: "mosaic",
 | 
			
		||||
        mosaic: "mosaic gallery",
 | 
			
		||||
        "mosaic gallery": "list",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const currentIndex = this.viewModes.indexOf(this.user.viewMode);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
			
		||||
      const data = {
 | 
			
		||||
        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.setItemWeight();
 | 
			
		||||
      //this.fillWindow();
 | 
			
		||||
    },
 | 
			
		||||
    preventDefault(event) {
 | 
			
		||||
      // Wrapper around prevent default.
 | 
			
		||||
| 
						 | 
				
			
			@ -387,7 +379,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        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;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from "vuex";
 | 
			
		||||
import { files as api } from "@/api";
 | 
			
		||||
import { theme } from "@/utils/constants";
 | 
			
		||||
import buttons from "@/utils/buttons";
 | 
			
		||||
import url from "@/utils/url";
 | 
			
		||||
import ace from "ace-builds/src-min-noconflict/ace.js";
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +52,7 @@ export default {
 | 
			
		|||
  created() {
 | 
			
		||||
    window.addEventListener("keydown", this.keyEvent);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    this.editor.destroy();
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@
 | 
			
		|||
          multiple
 | 
			
		||||
        />
 | 
			
		||||
      </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 class="item header">
 | 
			
		||||
            <div></div>
 | 
			
		||||
| 
						 | 
				
			
			@ -132,8 +132,11 @@
 | 
			
		|||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <h2 v-if="req.numDirs > 0">{{ $t("files.folders") }}</h2>
 | 
			
		||||
        <div v-if="req.numDirs > 0">
 | 
			
		||||
          <div class="header-items">
 | 
			
		||||
            <h2>{{ $t("files.folders") }}</h2>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="req.numDirs > 0">
 | 
			
		||||
          <item
 | 
			
		||||
            v-for="item in dirs"
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +153,11 @@
 | 
			
		|||
          </item>
 | 
			
		||||
        </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">
 | 
			
		||||
          <item
 | 
			
		||||
            v-for="item in files"
 | 
			
		||||
| 
						 | 
				
			
			@ -201,11 +208,19 @@
 | 
			
		|||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 | 
			
		||||
.header-items {
 | 
			
		||||
  width: 100% !important;
 | 
			
		||||
  max-width: 100% !important;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import { mapState, mapGetters, mapMutations } from "vuex";
 | 
			
		||||
import { users, files as api } from "@/api";
 | 
			
		||||
import { enableExec } from "@/utils/constants";
 | 
			
		||||
import * as upload from "@/utils/upload";
 | 
			
		||||
import css from "@/utils/css";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
| 
						 | 
				
			
			@ -291,11 +306,15 @@ export default {
 | 
			
		|||
    viewIcon() {
 | 
			
		||||
      const icons = {
 | 
			
		||||
        list: "view_module",
 | 
			
		||||
        mosaic: "grid_view",
 | 
			
		||||
        "mosaic gallery": "view_list",
 | 
			
		||||
        compact: "view_module",
 | 
			
		||||
        normal: "grid_view",
 | 
			
		||||
        gallery: "view_list",
 | 
			
		||||
      };
 | 
			
		||||
      return icons[this.user.viewMode];
 | 
			
		||||
    },
 | 
			
		||||
    listingViewMode() {
 | 
			
		||||
      return this.user.viewMode
 | 
			
		||||
    },
 | 
			
		||||
    headerButtons() {
 | 
			
		||||
      return {
 | 
			
		||||
        select: this.selectedCount > 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -345,7 +364,7 @@ export default {
 | 
			
		|||
    document.addEventListener("dragleave", this.dragLeave);
 | 
			
		||||
    document.addEventListener("drop", this.drop);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    // Remove event listeners before destroying this page.
 | 
			
		||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
			
		||||
    window.removeEventListener("scroll", this.scrollEvent);
 | 
			
		||||
| 
						 | 
				
			
			@ -528,7 +547,7 @@ export default {
 | 
			
		|||
      let columns = Math.floor(
 | 
			
		||||
        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;
 | 
			
		||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,8 +106,6 @@ import { files as api } from "@/api";
 | 
			
		|||
import { resizePreview } from "@/utils/constants";
 | 
			
		||||
import url from "@/utils/url";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import HeaderBar from "@/components/header/HeaderBar";
 | 
			
		||||
import Action from "@/components/header/Action";
 | 
			
		||||
import ExtendedImage from "@/components/files/ExtendedImage";
 | 
			
		||||
 | 
			
		||||
const mediaTypes = ["image", "video", "audio", "blob"];
 | 
			
		||||
| 
						 | 
				
			
			@ -115,8 +113,6 @@ const mediaTypes = ["image", "video", "audio", "blob"];
 | 
			
		|||
export default {
 | 
			
		||||
  name: "preview",
 | 
			
		||||
  components: {
 | 
			
		||||
    HeaderBar,
 | 
			
		||||
    Action,
 | 
			
		||||
    ExtendedImage,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +172,7 @@ export default {
 | 
			
		|||
    this.listing = this.oldReq.items;
 | 
			
		||||
    this.updatePreview();
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    window.removeEventListener("keydown", this.key);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,15 +71,6 @@
 | 
			
		|||
            {{ $t("settings.disableUsedDiskPercentage") }}
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p>
 | 
			
		||||
            <label for="theme">{{ $t("settings.themes.title") }}</label>
 | 
			
		||||
            <themes
 | 
			
		||||
              class="input input--block"
 | 
			
		||||
              :theme.sync="settings.frontend.theme"
 | 
			
		||||
              id="theme"
 | 
			
		||||
            ></themes>
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p>
 | 
			
		||||
            <label for="branding-name">{{ $t("settings.instanceName") }}</label>
 | 
			
		||||
            <input
 | 
			
		||||
| 
						 | 
				
			
			@ -194,13 +185,11 @@ import { settings as api } from "@/api";
 | 
			
		|||
import { enableExec } from "@/utils/constants";
 | 
			
		||||
import UserForm from "@/components/settings/UserForm";
 | 
			
		||||
import Rules from "@/components/settings/Rules";
 | 
			
		||||
import Themes from "@/components/settings/Themes";
 | 
			
		||||
import Errors from "@/views/Errors";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "settings",
 | 
			
		||||
  components: {
 | 
			
		||||
    Themes,
 | 
			
		||||
    UserForm,
 | 
			
		||||
    Rules,
 | 
			
		||||
    Errors,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,10 @@
 | 
			
		|||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="card-content">
 | 
			
		||||
          <p>
 | 
			
		||||
            <input type="checkbox" v-model="darkMode" />
 | 
			
		||||
            Dark Mode
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            <input type="checkbox" v-model="hideDotfiles" />
 | 
			
		||||
            {{ $t("settings.hideDotfiles") }}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +23,11 @@
 | 
			
		|||
            <input type="checkbox" v-model="dateFormat" />
 | 
			
		||||
            {{ $t("settings.setDateFormat") }}
 | 
			
		||||
          </p>
 | 
			
		||||
          <h3>Listing View Style</h3>
 | 
			
		||||
          <ViewMode
 | 
			
		||||
            class="input input--block"
 | 
			
		||||
            :viewMode.sync="viewMode"
 | 
			
		||||
          ></ViewMode>
 | 
			
		||||
          <h3>{{ $t("settings.language") }}</h3>
 | 
			
		||||
          <languages
 | 
			
		||||
            class="input input--block"
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +84,13 @@
 | 
			
		|||
import { mapState, mapMutations } from "vuex";
 | 
			
		||||
import { users as api } from "@/api";
 | 
			
		||||
import Languages from "@/components/settings/Languages";
 | 
			
		||||
import ViewMode from "@/components/settings/ViewMode";
 | 
			
		||||
import i18n, { rtlLanguages } from "@/i18n";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "settings",
 | 
			
		||||
  components: {
 | 
			
		||||
    ViewMode,
 | 
			
		||||
    Languages,
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +100,8 @@ export default {
 | 
			
		|||
      hideDotfiles: false,
 | 
			
		||||
      singleClick: false,
 | 
			
		||||
      dateFormat: false,
 | 
			
		||||
      darkMode: false,
 | 
			
		||||
      viewMode: "list",
 | 
			
		||||
      locale: "",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -109,8 +122,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    if (typeof this.user.darkMode === 'undefined') {
 | 
			
		||||
      this.darkMode = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.darkMode = this.user.darkMode
 | 
			
		||||
    }
 | 
			
		||||
    this.setLoading(false);
 | 
			
		||||
    this.locale = this.user.locale;
 | 
			
		||||
    this.viewMode = this.user.viewMode;
 | 
			
		||||
    this.hideDotfiles = this.user.hideDotfiles;
 | 
			
		||||
    this.singleClick = this.user.singleClick;
 | 
			
		||||
    this.dateFormat = this.user.dateFormat;
 | 
			
		||||
| 
						 | 
				
			
			@ -135,11 +154,12 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    async updateSettings(event) {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const data = {
 | 
			
		||||
          id: this.user.id,
 | 
			
		||||
          locale: this.locale,
 | 
			
		||||
          darkMode: this.darkMode,
 | 
			
		||||
          viewMode: this.viewMode,
 | 
			
		||||
          hideDotfiles: this.hideDotfiles,
 | 
			
		||||
          singleClick: this.singleClick,
 | 
			
		||||
          dateFormat: this.dateFormat,
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +169,8 @@ export default {
 | 
			
		|||
          rtlLanguages.includes(i18n.locale);
 | 
			
		||||
        await api.update(data, [
 | 
			
		||||
          "locale",
 | 
			
		||||
          "darkMode",
 | 
			
		||||
          "viewMode",
 | 
			
		||||
          "hideDotfiles",
 | 
			
		||||
          "singleClick",
 | 
			
		||||
          "dateFormat",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -107,7 +107,7 @@ export default {
 | 
			
		|||
      this.$showSuccess(this.$t("success.linkCopied"));
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.clip.destroy();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue