diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index cbe3f782..740eefdb 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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: diff --git a/.github/workflows/pr-merge.yaml b/.github/workflows/pr-merge.yaml index dca6c543..bfc65e06 100644 --- a/.github/workflows/pr-merge.yaml +++ b/.github/workflows/pr-merge.yaml @@ -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: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 175e205c..e620ec77 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5df9a358..9d2bd6e3 100644 --- a/Dockerfile +++ b/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" ] \ No newline at end of file diff --git a/backend/auth/hook.go b/backend/auth/hook.go index 0ae5d143..c1902947 100644 --- a/backend/auth/hook.go +++ b/backend/auth/hook.go @@ -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, } diff --git a/backend/auth/json.go b/backend/auth/json.go index d3734789..7344cd04 100644 --- a/backend/auth/json.go +++ b/backend/auth/json.go @@ -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 diff --git a/backend/cmd/hash.go b/backend/cmd/hash.go deleted file mode 100644 index 41718afa..00000000 --- a/backend/cmd/hash.go +++ /dev/null @@ -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 ", - 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) - }, -} diff --git a/backend/cmd/root.go b/backend/cmd/root.go index e64dbac1..8af247d4 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -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) } diff --git a/backend/cmd/users_add.go b/backend/cmd/users_add.go index 5eeb20e0..0bab2488 100644 --- a/backend/cmd/users_add.go +++ b/backend/cmd/users_add.go @@ -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() diff --git a/backend/cmd/users_update.go b/backend/cmd/users_update.go index b93f5284..f0e0a791 100644 --- a/backend/cmd/users_update.go +++ b/backend/cmd/users_update.go @@ -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) diff --git a/backend/cmd/utils.go b/backend/cmd/utils.go index bb9eea37..cae9854b 100644 --- a/backend/cmd/utils.go +++ b/backend/cmd/utils.go @@ -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 -} diff --git a/backend/filebrowser.yaml b/backend/filebrowser.yaml index 67755b08..ad03cd69 100644 --- a/backend/filebrowser.yaml +++ b/backend/filebrowser.yaml @@ -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 \ No newline at end of file + 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 diff --git a/backend/http/auth.go b/backend/http/auth.go index 2a42e05b..55af1b20 100644 --- a/backend/http/auth.go +++ b/backend/http/auth.go @@ -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)), diff --git a/backend/http/search.go b/backend/http/search.go index 22b6df6b..3ce8cc33 100644 --- a/backend/http/search.go +++ b/backend/http/search.go @@ -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, diff --git a/backend/http/share.go b/backend/http/share.go index 36f2e4c0..408cde49 100644 --- a/backend/http/share.go +++ b/backend/http/share.go @@ -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 } diff --git a/backend/http/static.go b/backend/http/static.go index aef7bd0d..fa06de3a 100644 --- a/backend/http/static.go +++ b/backend/http/static.go @@ -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, diff --git a/backend/http/users.go b/backend/http/users.go index cdcc58f1..20765293 100644 --- a/backend/http/users.go +++ b/backend/http/users.go @@ -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 diff --git a/backend/search/conditions.go b/backend/index/conditions.go similarity index 79% rename from backend/search/conditions.go rename to backend/index/conditions.go index b6791433..61c174c7 100644 --- a/backend/search/conditions.go +++ b/backend/index/conditions.go @@ -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 +} diff --git a/backend/index/indexing.go b/backend/index/indexing.go new file mode 100644 index 00000000..bfee12d3 --- /dev/null +++ b/backend/index/indexing.go @@ -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) + } +} diff --git a/backend/index/indexing_test.go b/backend/index/indexing_test.go new file mode 100644 index 00000000..f1d081e3 --- /dev/null +++ b/backend/index/indexing_test.go @@ -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) + }) + } +} diff --git a/backend/index/search_index.go b/backend/index/search_index.go new file mode 100644 index 00000000..d3ace10e --- /dev/null +++ b/backend/index/search_index.go @@ -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) +} diff --git a/backend/index/search_index_test.go b/backend/index/search_index_test.go new file mode 100644 index 00000000..1069a264 --- /dev/null +++ b/backend/index/search_index_test.go @@ -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) + } + }) + } +} diff --git a/backend/search/indexing.go b/backend/search/indexing.go deleted file mode 100644 index a05a5749..00000000 --- a/backend/search/indexing.go +++ /dev/null @@ -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 -} diff --git a/backend/search/search_test.go b/backend/search/search_test.go deleted file mode 100644 index 7e8a44da..00000000 --- a/backend/search/search_test.go +++ /dev/null @@ -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]) -} diff --git a/backend/settings/config.go b/backend/settings/config.go index df16dd34..b7f7ef51 100644 --- a/backend/settings/config.go +++ b/backend/settings/config.go @@ -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 +} diff --git a/backend/settings/config_test.go b/backend/settings/config_test.go new file mode 100644 index 00000000..fb51566c --- /dev/null +++ b/backend/settings/config_test.go @@ -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) + } + }) + } +} diff --git a/backend/settings/dir_test.go b/backend/settings/dir_test.go new file mode 100644 index 00000000..1b3abc4e --- /dev/null +++ b/backend/settings/dir_test.go @@ -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) + } + }) + } +} diff --git a/backend/settings/storage.go b/backend/settings/storage.go index 19c8fdcc..f54acc62 100644 --- a/backend/settings/storage.go +++ b/backend/settings/storage.go @@ -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 { diff --git a/backend/settings/structs.go b/backend/settings/structs.go index e1c26fcc..9bed8a92 100644 --- a/backend/settings/structs.go +++ b/backend/settings/structs.go @@ -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"` } diff --git a/backend/settings/testingConfig.yaml b/backend/settings/testingConfig.yaml index 76c4fd21..3db68736 100644 --- a/backend/settings/testingConfig.yaml +++ b/backend/settings/testingConfig.yaml @@ -28,12 +28,13 @@ frontend: disableExternal: true disableUsedPercentage: true files: "" - theme: "" color: "" userDefaults: scope: "" locale: "" viewMode: "" + darkMode: true + disableSettings: false singleClick: true sorting: by: "" diff --git a/backend/storage/bolt/users.go b/backend/storage/bolt/users.go index dc601f86..33a2b092 100644 --- a/backend/storage/bolt/users.go +++ b/backend/storage/bolt/users.go @@ -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 diff --git a/backend/users/password.go b/backend/users/password.go index d7ef250a..e5b5f72b 100644 --- a/backend/users/password.go +++ b/backend/users/password.go @@ -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 } diff --git a/backend/users/storage.go b/backend/users/storage.go index de939586..31aaa7ba 100644 --- a/backend/users/storage.go +++ b/backend/users/storage.go @@ -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 } diff --git a/backend/users/users.go b/backend/users/users.go index 84a0cab9..50ef124b 100644 --- a/backend/users/users.go +++ b/backend/users/users.go @@ -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 diff --git a/backend/version/version.go b/backend/version/version.go index 99e9b441..fb0df987 100644 --- a/backend/version/version.go +++ b/backend/version/version.go @@ -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)" ) diff --git a/configuration.md b/configuration.md index dc5b5762..802ef80c 100644 --- a/configuration.md +++ b/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`) diff --git a/frontend/.eslintrc.yml b/frontend/.eslintrc.yml new file mode 100644 index 00000000..e536ba99 --- /dev/null +++ b/frontend/.eslintrc.yml @@ -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 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4f2ebecd..dd0cec37 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,9 +35,20 @@ "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" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@achrinza/node-ipc": { "version": "9.2.7", "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.7.tgz", @@ -266,6 +277,84 @@ "node": ">=10.0.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -281,6 +370,39 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1140,6 +1262,15 @@ "acorn": "^8" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -1287,6 +1418,12 @@ } ] }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -2435,6 +2572,12 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/deepmerge": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", @@ -2655,6 +2798,18 @@ "node": ">=6" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -2869,6 +3024,114 @@ "node": ">=0.8.0" } }, + "node_modules/eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz", + "integrity": "sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.13", + "semver": "^7.5.4", + "vue-eslint-parser": "^9.3.1", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -2881,6 +3144,251 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -2908,6 +3416,15 @@ "node": ">=4.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3059,6 +3576,12 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -3092,6 +3615,18 @@ "node": ">=4" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -3238,6 +3773,26 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -3400,6 +3955,33 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -3433,6 +4015,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -3793,6 +4381,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3920,6 +4517,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -4061,6 +4667,24 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4078,6 +4702,12 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -4102,6 +4732,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4139,6 +4778,19 @@ "launch-editor": "^2.6.0" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -4241,6 +4893,12 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lodash.pullall": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz", @@ -4699,6 +5357,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -4944,6 +5608,23 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -5779,6 +6460,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -6738,6 +7428,18 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -6916,6 +7618,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7088,6 +7796,18 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -7239,6 +7959,88 @@ "vue": "~2" } }, + "node_modules/vue-eslint-parser": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz", + "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/vue-hot-reload-api": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", @@ -7774,6 +8576,15 @@ } } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7824,6 +8635,18 @@ "engines": { "node": ">=10" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 87f6487c..e5288f61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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": { diff --git a/frontend/public/index.html b/frontend/public/index.html index 00bd9650..d8cbd347 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -128,8 +128,8 @@ - [{[ if .Theme -]}] - + [{[ if .darkMode -]}] + [{[ end ]}] [{[ if .CSS -]}] diff --git a/frontend/public/themes/dark.css b/frontend/public/themes/dark.css deleted file mode 100644 index 42eb58ff..00000000 --- a/frontend/public/themes/dark.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5c4ac2b0..83193bb3 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,18 +1,20 @@ diff --git a/frontend/src/components/Search.vue b/frontend/src/components/Search.vue index c3cfe898..bed88010 100644 --- a/frontend/src/components/Search.vue +++ b/frontend/src/components/Search.vue @@ -1,5 +1,5 @@