V0.2.9 release (#205)

This commit is contained in:
Graham Steffaniak 2024-09-16 16:01:16 -05:00 committed by GitHub
parent 0bad14b51e
commit 62d1cd88a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 1843 additions and 1355 deletions

View File

@ -12,6 +12,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Find latest tag
run: |
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
echo "latest tag is $LATEST_TAG"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -31,7 +35,7 @@ jobs:
with: with:
context: . context: .
build-args: | build-args: |
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} VERSION=${{ env.LATEST_TAG }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile file: ./Dockerfile

View File

@ -52,5 +52,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
version=${{ steps.meta.outputs.version }} VERSION=${{ steps.meta.outputs.version }}
commitSHA=${{ steps.meta.outputs.revision }} REVISION=${{ steps.meta.outputs.revision }}

View File

@ -35,7 +35,7 @@ jobs:
JSON="${{ steps.meta.outputs.tags }}" JSON="${{ steps.meta.outputs.tags }}"
# Use jq to remove 'v' from the version field # Use jq to remove 'v' from the version field
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/') JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT echo "CLEANED_TAG=$JSON" >> $GITHUB_ENV
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@ -46,5 +46,5 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }} tags: ${{ env.CLEANED_TAG }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.2.9
This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
**New Features**:
- listing view items are middle-clickable on selected listing or when in single-click mode.
- listing view items can be navigated via arrow keys.
- listing view can jump to items using letters and number keys to cycle through files that start with that character.
- You can use the enter key and backspace key to navigate backwards and forwards on selected items.
- ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
- Added right-click context menu to replace the file selection prompt.
**Bugfixes**:
- Fixed drag to upload not working.
- Fixed shared video link issues.
- Fixed user edit bug related to other user.
- Fixed password reset bug.
- Fixed loading state getting stuck.
## v0.2.8 ## v0.2.8
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141)) - **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))

View File

@ -4,7 +4,7 @@
<p align="center"> <p align="center">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL"> <img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
</p> </p>
<h3 align="center">Filebrowser Quantum - A modern web-based file manager</h3> <h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
<p align="center"> <p align="center">
<img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot"> <img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
</p> </p>
@ -15,7 +15,7 @@
> Starting with v0.2.4 *ALL* share links need to be re-created (due to > Starting with v0.2.4 *ALL* share links need to be re-created (due to
> security fix). > security fix).
Filebrowser Quantum is a fork of the filebrowser opensource project with the FileBrowser Quantum is a fork of the filebrowser opensource project with the
following changes: following changes:
1. [x] Enhanced lightning fast indexed search 1. [x] Enhanced lightning fast indexed search
@ -33,7 +33,7 @@ following changes:
## About ## About
Filebrowser Quantum provides a file managing interface within a specified directory FileBrowser Quantum provides a file managing interface within a specified directory
and can be used to upload, delete, preview, rename, and edit your files. and can be used to upload, delete, preview, rename, and edit your files.
It allows the creation of multiple users and each user can have its It allows the creation of multiple users and each user can have its
directory. directory.
@ -44,7 +44,7 @@ aesthetics and performance. Improved search, simplified ui
(without removing features) and more secure and up-to-date (without removing features) and more secure and up-to-date
build are just a few examples. build are just a few examples.
Filebrowser Quantum differs significantly to the original. FileBrowser Quantum differs significantly to the original.
There are hundreds of thousands of lines changed and they are generally There are hundreds of thousands of lines changed and they are generally
no longer compatible with each other. This has been intentional -- the no longer compatible with each other. This has been intentional -- the
focus of this fork is on a few key principles: focus of this fork is on a few key principles:

View File

@ -187,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
func (a *HookAuth) GetUser(d *users.User) *users.User { func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin // adds all permissions when user is admin
isAdmin := d.Perm.Admin isAdmin := d.Perm.Admin
perms := users.Permissions{ perms := settings.Permissions{
Admin: isAdmin, Admin: isAdmin,
Execute: isAdmin || d.Perm.Execute, Execute: isAdmin || d.Perm.Execute,
Create: isAdmin || d.Perm.Create, Create: isAdmin || d.Perm.Create,

View File

@ -5,49 +5,37 @@
? github.com/gtsteffaniak/filebrowser/auth [no test files] ? github.com/gtsteffaniak/filebrowser/auth [no test files]
? github.com/gtsteffaniak/filebrowser/cmd [no test files] ? github.com/gtsteffaniak/filebrowser/cmd [no test files]
PASS PASS
ok github.com/gtsteffaniak/filebrowser/diskcache 0.003s ok github.com/gtsteffaniak/filebrowser/diskcache 0.004s
? github.com/gtsteffaniak/filebrowser/errors [no test files] ? github.com/gtsteffaniak/filebrowser/errors [no test files]
goos: linux goos: linux
goarch: amd64 goarch: amd64
pkg: github.com/gtsteffaniak/filebrowser/files pkg: github.com/gtsteffaniak/filebrowser/files
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
BenchmarkFillIndex-8 10 3587120 ns/op 273640 B/op 2013 allocs/op BenchmarkFillIndex-8 10 3559830 ns/op 274639 B/op 2026 allocs/op
BenchmarkSearchAllIndexes-8 10 31291180 ns/op 19500700 B/op 298636 allocs/op BenchmarkSearchAllIndexes-8 10 31912612 ns/op 20545741 B/op 312477 allocs/op
PASS PASS
ok github.com/gtsteffaniak/filebrowser/files 0.408s ok github.com/gtsteffaniak/filebrowser/files 0.417s
PASS PASS
ok github.com/gtsteffaniak/filebrowser/fileutils 0.003s ok github.com/gtsteffaniak/filebrowser/fileutils 0.002s
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser 2024/08/27 16:16:13 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 Saving new user: publicUser
2024/02/07 07:16:43 h: 401 <nil>
2024/02/07 07:16:43 h: 401 <nil>
PASS PASS
ok github.com/gtsteffaniak/filebrowser/http 0.202s ok github.com/gtsteffaniak/filebrowser/http 0.100s
PASS PASS
ok github.com/gtsteffaniak/filebrowser/img 0.125s ok github.com/gtsteffaniak/filebrowser/img 0.124s
PASS PASS
ok github.com/gtsteffaniak/filebrowser/rules 0.002s ok github.com/gtsteffaniak/filebrowser/rules 0.002s
PASS PASS
ok github.com/gtsteffaniak/filebrowser/runner 0.003s ok github.com/gtsteffaniak/filebrowser/runner 0.003s
PASS PASS
ok github.com/gtsteffaniak/filebrowser/settings 0.005s ok github.com/gtsteffaniak/filebrowser/settings 0.004s
? github.com/gtsteffaniak/filebrowser/share [no test files] ? github.com/gtsteffaniak/filebrowser/share [no test files]
? github.com/gtsteffaniak/filebrowser/storage [no test files] ? github.com/gtsteffaniak/filebrowser/storage [no test files]
? github.com/gtsteffaniak/filebrowser/storage/bolt [no test files] ? github.com/gtsteffaniak/filebrowser/storage/bolt [no test files]
PASS PASS
ok github.com/gtsteffaniak/filebrowser/users 0.003s ok github.com/gtsteffaniak/filebrowser/users 0.002s
? github.com/gtsteffaniak/filebrowser/version [no test files] ? github.com/gtsteffaniak/filebrowser/version [no test files]

View File

@ -26,6 +26,7 @@ import (
"github.com/gtsteffaniak/filebrowser/img" "github.com/gtsteffaniak/filebrowser/img"
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/settings"
"github.com/gtsteffaniak/filebrowser/users" "github.com/gtsteffaniak/filebrowser/users"
"github.com/gtsteffaniak/filebrowser/version"
) )
//go:embed dist/* //go:embed dist/*
@ -47,7 +48,7 @@ func init() {
// Bind the flags to the pflag command line parser // Bind the flags to the pflag command line parser
pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse() pflag.Parse()
log.Println("Initializing with config file:", *configFlag) log.Printf("Initializing FileBrowser Quantum (%v) with config file: %v \n", version.Version, *configFlag)
log.Println("Embeded Frontend:", !nonEmbededFS) log.Println("Embeded Frontend:", !nonEmbededFS)
settings.Initialize(*configFlag) settings.Initialize(*configFlag)
} }
@ -162,8 +163,7 @@ func quickSetup(d pythonData) {
checkErr("d.store.Settings.Save", err) checkErr("d.store.Settings.Save", err)
err = d.store.Settings.SaveServer(&settings.Config.Server) err = d.store.Settings.SaveServer(&settings.Config.Server)
checkErr("d.store.Settings.SaveServer", err) checkErr("d.store.Settings.SaveServer", err)
user := &users.User{} user := users.ApplyDefaults(users.User{})
settings.Config.UserDefaults.Apply(user)
user.Username = settings.Config.Auth.AdminUsername user.Username = settings.Config.Auth.AdminUsername
user.Password = settings.Config.Auth.AdminPassword user.Password = settings.Config.Auth.AdminPassword
user.Perm.Admin = true user.Perm.Admin = true
@ -171,7 +171,7 @@ func quickSetup(d pythonData) {
user.DarkMode = true user.DarkMode = true
user.ViewMode = "normal" user.ViewMode = "normal"
user.LockPassword = false user.LockPassword = false
user.Perm = users.Permissions{ user.Perm = settings.Permissions{
Create: true, Create: true,
Rename: true, Rename: true,
Modify: true, Modify: true,
@ -180,6 +180,6 @@ func quickSetup(d pythonData) {
Download: true, Download: true,
Admin: true, Admin: true,
} }
err = d.store.Users.Save(user) err = d.store.Users.Save(&user)
checkErr("d.store.Users.Save", err) checkErr("d.store.Users.Save", err)
} }

View File

@ -67,11 +67,6 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
} }
func printRules(rulez []rules.Rule, id interface{}) { func printRules(rulez []rules.Rule, id interface{}) {
if id == nil {
fmt.Printf("Global Rules:\n\n")
} else {
fmt.Printf("Rules for user %v:\n\n", id)
}
for id, rule := range rulez { for id, rule := range rulez {
fmt.Printf("(%d) ", id) fmt.Printf("(%d) ", id)

View File

@ -1,7 +1,7 @@
package cmd package cmd
import ( import (
"fmt" "log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -26,6 +26,6 @@ var usersRmCmd = &cobra.Command{
} }
checkErr("usersRmCmd", err) checkErr("usersRmCmd", err)
fmt.Println("user deleted successfully") log.Println("user deleted successfully")
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@ -16,6 +16,6 @@ var versionCmd = &cobra.Command{
Use: "version", Use: "version",
Short: "Print the version number", Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA) fmt.Println("File Browser " + version.Version + "/" + version.CommitSHA)
}, },
} }

View File

@ -1,8 +1,8 @@
package files package files
import ( import (
"crypto/md5" //nolint:gosec "crypto/md5"
"crypto/sha1" //nolint:gosec "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
@ -52,6 +52,7 @@ type FileInfo struct {
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
type FileOptions struct { type FileOptions struct {
Path string // realpath Path string // realpath
IsDir bool
Modify bool Modify bool
Expand bool Expand bool
ReadHeader bool ReadHeader bool
@ -83,7 +84,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) { if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
file, err := stat(opts.Path, opts) // Pass opts.Path here file, err := stat(opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -101,7 +102,6 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
} }
return file, err return file, err
} }
func FileInfoFaster(opts FileOptions) (*FileInfo, error) { func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
// Lock access for the specific path // Lock access for the specific path
pathMutex := getMutex(opts.Path) pathMutex := getMutex(opts.Path)
@ -111,71 +111,65 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
index := GetIndex(rootPath) index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/") adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
if trimmed == "" { if opts.IsDir {
trimmed = "/" info, exists := index.GetMetadataInfo(adjustedPath)
if exists && !opts.Content {
// Let's not refresh if less than a second has passed
if time.Since(info.CacheTime) > time.Second {
go RefreshFileInfo(opts) //nolint:errcheck
}
// refresh cache after
return &info, nil
}
}
// don't bother caching content
if opts.Content {
file, err := NewFileInfo(opts)
return file, err
}
err := RefreshFileInfo(opts)
if err != nil {
file, err := NewFileInfo(opts)
return file, err
} }
adjustedPath := makeIndexPath(trimmed, index.Root)
var info FileInfo
info, exists := index.GetMetadataInfo(adjustedPath) info, exists := index.GetMetadataInfo(adjustedPath)
if exists && !opts.Content { if !exists || info.Name == "" {
// Check if the cache time is less than 1 second return &FileInfo{}, errors.ErrEmptyKey
if time.Since(info.CacheTime) > time.Second {
go RefreshFileInfo(opts)
}
// refresh cache after
return &info, nil
} else {
// don't bother caching content
if opts.Content {
file, err := NewFileInfo(opts)
return file, err
}
updated := RefreshFileInfo(opts)
if !updated {
file, err := NewFileInfo(opts)
return file, err
}
info, exists = index.GetMetadataInfo(adjustedPath)
if !exists || info.Name == "" {
return &FileInfo{}, errors.ErrEmptyKey
}
return &info, nil
} }
return &info, nil
} }
func RefreshFileInfo(opts FileOptions) bool { func RefreshFileInfo(opts FileOptions) error {
if !opts.Checker.Check(opts.Path) { if !opts.Checker.Check(opts.Path) {
return false return fmt.Errorf("permission denied: %s", opts.Path)
} }
index := GetIndex(rootPath) index := GetIndex(rootPath)
trimmed := strings.TrimPrefix(opts.Path, "/") adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
if trimmed == "" { file, err := stat(opts)
trimmed = "/"
}
adjustedPath := makeIndexPath(trimmed, index.Root)
file, err := stat(opts.Path, opts) // Pass opts.Path here
if err != nil { if err != nil {
return false return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
} }
_ = file.detectType(adjustedPath, true, opts.Content, opts.ReadHeader) _ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
if file.IsDir { if file.IsDir {
err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader) err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
if err != nil { if err != nil {
return false return fmt.Errorf("Dir info could not be read: %s", opts.Path)
} }
return index.UpdateFileMetadata(adjustedPath, *file)
} else {
return index.UpdateFileMetadata(adjustedPath, *file)
} }
result := index.UpdateFileMetadata(adjustedPath, *file)
if !result {
return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
}
return nil
} }
func stat(path string, opts FileOptions) (*FileInfo, error) { func stat(opts FileOptions) (*FileInfo, error) {
info, err := os.Lstat(path) info, err := os.Lstat(opts.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
file := &FileInfo{ file := &FileInfo{
Path: opts.Path, Path: opts.Path,
Name: info.Name(), Name: info.Name(),
@ -185,13 +179,12 @@ func stat(path string, opts FileOptions) (*FileInfo, error) {
Extension: filepath.Ext(info.Name()), Extension: filepath.Ext(info.Name()),
Token: opts.Token, Token: opts.Token,
} }
if info.IsDir() { if info.IsDir() {
file.IsDir = true file.IsDir = true
} }
if info.Mode()&os.ModeSymlink != 0 { if info.Mode()&os.ModeSymlink != 0 {
file.IsSymlink = true file.IsSymlink = true
targetInfo, err := os.Stat(path) targetInfo, err := os.Stat(opts.Path)
if err == nil { if err == nil {
file.Size = targetInfo.Size() file.Size = targetInfo.Size()
file.IsDir = targetInfo.IsDir() file.IsDir = targetInfo.IsDir()
@ -248,20 +241,19 @@ func (i *FileInfo) RealPath() string {
return i.Path return i.Path
} }
func GetRealPath(relativePath ...string) (string, error) { func GetRealPath(relativePath ...string) (string, bool, error) {
combined := []string{settings.Config.Server.Root} combined := []string{settings.Config.Server.Root}
for _, path := range relativePath { for _, path := range relativePath {
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root)) combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
} }
joinedPath := filepath.Join(combined...) joinedPath := filepath.Join(combined...)
// Convert relative path to absolute path // Convert relative path to absolute path
absolutePath, err := filepath.Abs(joinedPath) absolutePath, err := filepath.Abs(joinedPath)
if err != nil { if err != nil {
return "", err return "", false, err
} }
if !Exists(absolutePath) { if !Exists(absolutePath) {
return absolutePath, nil // return without error return absolutePath, false, nil // return without error
} }
// Resolve symlinks and get the real path // Resolve symlinks and get the real path
return resolveSymlinks(absolutePath) return resolveSymlinks(absolutePath)
@ -272,10 +264,9 @@ func DeleteFiles(absPath string, opts FileOptions) error {
if err != nil { if err != nil {
return err return err
} }
parentDir := filepath.Dir(absPath) opts.Path = filepath.Dir(absPath)
opts.Path = parentDir err = RefreshFileInfo(opts)
updated := RefreshFileInfo(opts) if err != nil {
if !updated {
return errors.ErrEmptyKey return errors.ErrEmptyKey
} }
return nil return nil
@ -288,16 +279,14 @@ func WriteDirectory(opts FileOptions) error {
return err return err
} }
opts.Path = filepath.Dir(opts.Path) opts.Path = filepath.Dir(opts.Path)
updated := RefreshFileInfo(opts) err = RefreshFileInfo(opts)
if !updated { if err != nil {
return errors.ErrEmptyKey return errors.ErrEmptyKey
} }
return nil return nil
} }
func WriteFile(opts FileOptions, in io.Reader) error { func WriteFile(opts FileOptions, in io.Reader) error {
fmt.Println("writing file", opts.Path)
dst := opts.Path dst := opts.Path
parentDir := filepath.Dir(dst) parentDir := filepath.Dir(dst)
// Split the directory from the destination path // Split the directory from the destination path
@ -321,23 +310,21 @@ func WriteFile(opts FileOptions, in io.Reader) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println("refreshing info for ", parentDir)
opts.Path = parentDir opts.Path = parentDir
updated := RefreshFileInfo(opts) err = RefreshFileInfo(opts)
if !updated { if err != nil {
return errors.ErrEmptyKey return errors.ErrEmptyKey
} }
return nil return nil
} }
// resolveSymlinks resolves symlinks in the given path // resolveSymlinks resolves symlinks in the given path
func resolveSymlinks(path string) (string, error) { func resolveSymlinks(path string) (string, bool, error) {
for { for {
// Get the file info // Get the file info
info, err := os.Lstat(path) info, err := os.Lstat(path)
if err != nil { if err != nil {
return "", err return "", false, err
} }
// Check if it's a symlink // Check if it's a symlink
@ -345,14 +332,14 @@ func resolveSymlinks(path string) (string, error) {
// Read the symlink target // Read the symlink target
target, err := os.Readlink(path) target, err := os.Readlink(path)
if err != nil { if err != nil {
return "", err return "", false, err
} }
// Resolve the target relative to the symlink's directory // Resolve the target relative to the symlink's directory
path = filepath.Join(filepath.Dir(path), target) path = filepath.Join(filepath.Dir(path), target)
} else { } else {
// Not a symlink, so we are done // Not a symlink, so return the resolved path and check if it's a directory
return path, nil return path, info.IsDir(), nil
} }
} }
} }

View File

@ -0,0 +1,76 @@
package files
import (
"os"
"path/filepath"
"strings"
"testing"
)
func Test_GetRealPath(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
return
}
trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
tests := []struct {
name string
paths []string
want struct {
path string
isDir bool
}
}{
{
name: "current directory",
paths: []string{
"./",
},
want: struct {
path string
isDir bool
}{
path: "backend/files",
isDir: true,
},
},
{
name: "current directory",
paths: []string{
"./file.go",
},
want: struct {
path string
isDir bool
}{
path: "backend/files/file.go",
isDir: false,
},
},
{
name: "other test case",
paths: []string{
"/mnt/doesnt/exist",
},
want: struct {
path string
isDir bool
}{
path: "/mnt/doesnt/exist",
isDir: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
realPath, isDir, err := GetRealPath(tt.paths...)
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
}
if err != nil {
t.Error("got error", err)
}
})
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"log" "log"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -15,6 +16,7 @@ type Directory struct {
Metadata map[string]FileInfo Metadata map[string]FileInfo
Files string Files string
} }
type File struct { type File struct {
Name string Name string
IsDir bool IsDir bool
@ -80,8 +82,7 @@ func indexingScheduler(intervalMinutes uint32) {
// Define a function to recursively index files and directories // Define a function to recursively index files and directories
func (si *Index) indexFiles(path string) error { func (si *Index) indexFiles(path string) error {
// Check if the current directory has been modified since the last indexing // Check if the current directory has been modified since the last indexing
path = strings.TrimSuffix(path, "/") adjustedPath := si.makeIndexPath(path, true)
adjustedPath := makeIndexPath(path, si.Root)
dir, err := os.Open(path) dir, err := os.Open(path)
if err != nil { if err != nil {
// Directory must have been deleted, remove it from the index // Directory must have been deleted, remove it from the index
@ -114,7 +115,7 @@ func (si *Index) indexFiles(path string) error {
} }
func (si *Index) InsertFiles(path string) { func (si *Index) InsertFiles(path string) {
adjustedPath := makeIndexPath(path, si.Root) adjustedPath := si.makeIndexPath(path, false)
subDirectory := Directory{} subDirectory := Directory{}
buffer := bytes.Buffer{} buffer := bytes.Buffer{}
@ -130,9 +131,9 @@ func (si *Index) InsertFiles(path string) {
} }
func (si *Index) InsertDirs(path string) { func (si *Index) InsertDirs(path string) {
adjustedPath := makeIndexPath(path, si.Root)
for _, f := range si.GetQuickList() { for _, f := range si.GetQuickList() {
if f.IsDir { if f.IsDir {
adjustedPath := si.makeIndexPath(path, true)
if _, exists := si.Directories[adjustedPath]; exists { if _, exists := si.Directories[adjustedPath]; exists {
si.UpdateCount("dirs") si.UpdateCount("dirs")
// Add or update the directory in the map // Add or update the directory in the map
@ -154,14 +155,21 @@ func (si *Index) InsertDirs(path string) {
} }
} }
func makeIndexPath(path string, root string) string { func (si *Index) makeIndexPath(subPath string, isDir bool) string {
if path == root { if si.Root == subPath {
return "/" return "/"
} }
adjustedPath := strings.TrimPrefix(path, root+"/") // clean path
subPath = strings.TrimSuffix(subPath, "/")
// remove index prefix
adjustedPath := strings.TrimPrefix(subPath, si.Root)
// remove trailing slash
adjustedPath = strings.TrimSuffix(adjustedPath, "/") adjustedPath = strings.TrimSuffix(adjustedPath, "/")
// add leading slash for root of index
if adjustedPath == "" { if adjustedPath == "" {
adjustedPath = "/" adjustedPath = "/"
} else if !isDir {
adjustedPath = filepath.Dir(adjustedPath)
} }
return adjustedPath return adjustedPath
} }

View File

@ -8,24 +8,6 @@ import (
"github.com/gtsteffaniak/filebrowser/settings" "github.com/gtsteffaniak/filebrowser/settings"
) )
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
si.mu.RLock()
dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock()
if exists {
// Initialize the Metadata map if it is nil
if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir)
return FileInfo{}, false
} else {
return dir.Metadata[adjustedPath], true
}
}
return FileInfo{}, false
}
// UpdateFileMetadata updates the FileInfo for the specified directory in the index. // UpdateFileMetadata updates the FileInfo for the specified directory in the index.
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool { func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
si.mu.Lock() si.mu.Lock()
@ -45,7 +27,6 @@ func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
// SetFileMetadata sets the FileInfo for the specified directory in the index. // SetFileMetadata sets the FileInfo for the specified directory in the index.
// internal use only // internal use only
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool { func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
_, exists := si.Directories[adjustedPath] _, exists := si.Directories[adjustedPath]
if !exists { if !exists {
return false return false
@ -57,6 +38,7 @@ func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index. // GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) { func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
fi := FileInfo{}
si.mu.RLock() si.mu.RLock()
dir, exists := si.Directories[adjustedPath] dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock() si.mu.RUnlock()
@ -65,11 +47,11 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
if dir.Metadata == nil { if dir.Metadata == nil {
dir.Metadata = make(map[string]FileInfo) dir.Metadata = make(map[string]FileInfo)
si.SetDirectoryInfo(adjustedPath, dir) si.SetDirectoryInfo(adjustedPath, dir)
} else {
fi = dir.Metadata[adjustedPath]
} }
info, metadataExists := dir.Metadata[adjustedPath]
return info, metadataExists
} }
return FileInfo{}, false return fi, exists
} }
// SetDirectoryInfo sets the directory information in the index. // SetDirectoryInfo sets the directory information in the index.
@ -84,10 +66,7 @@ func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
si.mu.RLock() si.mu.RLock()
dir, exists := si.Directories[adjustedPath] dir, exists := si.Directories[adjustedPath]
si.mu.RUnlock() si.mu.RUnlock()
if exists { return dir, exists
return dir, true
}
return Directory{}, false
} }
func (si *Index) RemoveDirectory(path string) { func (si *Index) RemoveDirectory(path string) {

220
backend/files/sync_test.go Normal file
View File

@ -0,0 +1,220 @@
package files
import (
"io/fs"
"os"
"testing"
"time"
)
// Mock for fs.FileInfo
type mockFileInfo struct {
name string
isDir bool
}
func (m mockFileInfo) Name() string { return m.name }
func (m mockFileInfo) Size() int64 { return 0 }
func (m mockFileInfo) Mode() os.FileMode { return 0 }
func (m mockFileInfo) ModTime() time.Time { return time.Now() }
func (m mockFileInfo) IsDir() bool { return m.isDir }
func (m mockFileInfo) Sys() interface{} { return nil }
var testIndex Index
// Test for GetFileMetadata
//func TestGetFileMetadata(t *testing.T) {
// t.Parallel()
// tests := []struct {
// name string
// adjustedPath string
// fileName string
// expectedName string
// expectedExists bool
// }{
// {
// name: "testpath exists",
// adjustedPath: "/testpath",
// fileName: "testfile.txt",
// expectedName: "testfile.txt",
// expectedExists: true,
// },
// {
// name: "testpath not exists",
// adjustedPath: "/testpath",
// fileName: "nonexistent.txt",
// expectedName: "",
// expectedExists: false,
// },
// {
// name: "File exists in /anotherpath",
// adjustedPath: "/anotherpath",
// fileName: "afile.txt",
// expectedName: "afile.txt",
// expectedExists: true,
// },
// {
// name: "File does not exist in /anotherpath",
// adjustedPath: "/anotherpath",
// fileName: "nonexistentfile.txt",
// expectedName: "",
// expectedExists: false,
// },
// {
// name: "Directory does not exist",
// adjustedPath: "/nonexistentpath",
// fileName: "testfile.txt",
// expectedName: "",
// expectedExists: false,
// },
// }
//
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// fileInfo, exists := testIndex.GetFileMetadata(tt.adjustedPath)
// if exists != tt.expectedExists || fileInfo.Name != tt.expectedName {
// t.Errorf("expected %v:%v but got: %v:%v", tt.expectedName, tt.expectedExists, //fileInfo.Name, exists)
// }
// })
// }
//}
// Test for UpdateFileMetadata
func TestUpdateFileMetadata(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
},
}
info := FileInfo{Name: "testfile.txt"}
success := index.UpdateFileMetadata("/testpath", info)
if !success {
t.Fatalf("expected UpdateFileMetadata to succeed")
}
dir, exists := index.Directories["/testpath"]
if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected testfile.txt to be updated in the directory metadata")
}
}
// Test for GetDirMetadata
func TestGetDirMetadata(t *testing.T) {
t.Parallel()
_, exists := testIndex.GetMetadataInfo("/testpath")
if !exists {
t.Fatalf("expected GetDirMetadata to return initialized metadata map")
}
_, exists = testIndex.GetMetadataInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirMetadata to return false for nonexistent directory")
}
}
// Test for SetDirectoryInfo
func TestSetDirectoryInfo(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
},
}
dir := Directory{Metadata: map[string]FileInfo{"testfile.txt": {Name: "testfile.txt"}}}
index.SetDirectoryInfo("/newPath", dir)
storedDir, exists := index.Directories["/newPath"]
if !exists || storedDir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected SetDirectoryInfo to store directory info correctly")
}
}
// Test for GetDirectoryInfo
func TestGetDirectoryInfo(t *testing.T) {
t.Parallel()
dir, exists := testIndex.GetDirectoryInfo("/testpath")
if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
t.Fatalf("expected GetDirectoryInfo to return correct directory info")
}
_, exists = testIndex.GetDirectoryInfo("/nonexistent")
if exists {
t.Fatalf("expected GetDirectoryInfo to return false for nonexistent directory")
}
}
// Test for RemoveDirectory
func TestRemoveDirectory(t *testing.T) {
index := &Index{
Directories: map[string]Directory{
"/testpath": {},
},
}
index.RemoveDirectory("/testpath")
_, exists := index.Directories["/testpath"]
if exists {
t.Fatalf("expected directory to be removed")
}
}
// Test for UpdateCount
func TestUpdateCount(t *testing.T) {
index := &Index{}
index.UpdateCount("files")
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
if index.NumFiles != 1 {
t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
}
index.UpdateCount("dirs")
if index.NumDirs != 1 {
t.Fatalf("expected NumDirs to be 1 after UpdateCount('dirs')")
}
index.UpdateCount("unknown")
// Just ensure it does not panic or update any counters
if index.NumFiles != 1 || index.NumDirs != 1 {
t.Fatalf("expected counts to remain unchanged for unknown type")
}
index.resetCount()
if index.NumFiles != 0 || index.NumDirs != 0 || !index.inProgress {
t.Fatalf("expected resetCount to reset counts and set inProgress to true")
}
}
func init() {
testIndex = Index{
NumFiles: 10,
NumDirs: 5,
inProgress: false,
Directories: map[string]Directory{
"/testpath": {
Metadata: map[string]FileInfo{
"testfile.txt": {Name: "testfile.txt"},
"anotherfile.txt": {Name: "anotherfile.txt"},
},
},
"/anotherpath": {
Metadata: map[string]FileInfo{
"afile.txt": {Name: "afile.txt"},
},
},
},
}
files := []fs.FileInfo{
mockFileInfo{name: "file1.txt", isDir: false},
mockFileInfo{name: "dir1", isDir: true},
}
testIndex.UpdateQuickList(files)
}

View File

@ -47,7 +47,7 @@ require (
github.com/ulikunitz/xz v0.5.12 // indirect github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.10 // indirect go.etcd.io/bbolt v1.3.11 // indirect
golang.org/x/net v0.28.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect golang.org/x/sys v0.24.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect

View File

@ -2,7 +2,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
@ -33,8 +32,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU= github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
@ -42,11 +39,9 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -62,7 +57,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
@ -71,7 +65,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -85,12 +78,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@ -103,29 +94,21 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM= github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ= github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -143,7 +126,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@ -153,18 +135,13 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@ -174,15 +151,13 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -192,22 +167,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=

View File

@ -2,7 +2,6 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
user := &users.User{ user := users.ApplyDefaults(users.User{})
Username: info.Username, user.Username = info.Username
Password: info.Password, user.Password = info.Password
}
settings.Config.UserDefaults.Apply(user)
userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root) userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
if err != nil { if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
} }
user.Scope = userHome user.Scope = userHome
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome) log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
settings.Config.UserDefaults.Apply(user) err = d.store.Users.Save(&user)
err = d.store.Users.Save(user)
if err == errors.ErrExist { if err == errors.ErrExist {
return http.StatusConflict, err return http.StatusConflict, err
} else if err != nil { } else if err != nil {
@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) { func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime) duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
if err != nil { if err != nil {
fmt.Println("Error parsing duration:", err)
duration = time.Hour * 2 duration = time.Hour * 2
} }
claims := &authToken{ claims := &authToken{

View File

@ -19,25 +19,25 @@ import (
var withHashFile = func(fn handleFunc) handleFunc { var withHashFile = func(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
id, path := ifPathWithName(r) id, path := ifPathWithName(r)
fmt.Println(id, path)
link, err := d.store.Share.GetByHash(id) link, err := d.store.Share.GetByHash(id)
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
} }
if link.Hash != "" { if link.Hash != "" {
var status int var status int
status, err = authenticateShareRequest(r, link) // Assign to the existing `err` variable status, err = authenticateShareRequest(r, link)
if err != nil || status != 0 { if err != nil || status != 0 {
return status, err return status, err
} }
} }
d.user = &users.PublicUser d.user = &users.PublicUser
realPath, err := files.GetRealPath(d.user.Scope, link.Path, path) realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
file, err := files.FileInfoFaster(files.FileOptions{ file, err := files.FileInfoFaster(files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: true, Expand: true,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,

View File

@ -81,12 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
if !d.user.Perm.Download { if !d.user.Perm.Download {
return http.StatusAccepted, nil return http.StatusAccepted, nil
} }
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
file, err := files.FileInfoFaster(files.FileOptions{ file, err := files.FileInfoFaster(files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,

View File

@ -18,13 +18,13 @@ import (
) )
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
fmt.Println("unable to get real path", d.user.Scope, r.URL.Path)
return http.StatusNotFound, err return http.StatusNotFound, err
} }
file, err := files.FileInfoFaster(files.FileOptions{ file, err := files.FileInfoFaster(files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: true, Expand: true,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
@ -34,19 +34,16 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
} }
if file.IsDir { if !file.IsDir {
file.Listing.Sorting = d.user.Sorting if checksum := r.URL.Query().Get("checksum"); checksum != "" {
return renderJSON(w, r, file) err := file.Checksum(checksum)
} if err == errors.ErrInvalidOption {
if checksum := r.URL.Query().Get("checksum"); checksum != "" { return http.StatusBadRequest, nil
err := file.Checksum(checksum) } else if err != nil {
if err == errors.ErrInvalidOption { return http.StatusInternalServerError, err
return http.StatusBadRequest, nil }
} else if err != nil {
return http.StatusInternalServerError, err
} }
} }
return renderJSON(w, r, file) return renderJSON(w, r, file)
}) })
@ -55,12 +52,13 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
if r.URL.Path == "/" || !d.user.Perm.Delete { if r.URL.Path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
fileOpts := files.FileOptions{ fileOpts := files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
@ -90,12 +88,13 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
if !d.user.Perm.Create || !d.Check(r.URL.Path) { if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
fileOpts := files.FileOptions{ fileOpts := files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
@ -109,7 +108,6 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
} }
return http.StatusOK, nil return http.StatusOK, nil
} }
file, err := files.FileInfoFaster(fileOpts) file, err := files.FileInfoFaster(fileOpts)
if err == nil { if err == nil {
if r.URL.Query().Get("override") != "true" { if r.URL.Query().Get("override") != "true" {
@ -141,12 +139,13 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
fileOpts := files.FileOptions{ fileOpts := files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
@ -187,7 +186,6 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
err = d.RunHook(func() error { err = d.RunHook(func() error {
fmt.Println("hook", src, dst)
return patchAction(r.Context(), action, src, dst, d, fileCache) return patchAction(r.Context(), action, src, dst, d, fileCache)
}, action, src, dst, d.user) }, action, src, dst, d.user)
@ -237,16 +235,17 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
} }
src = path.Clean("/" + src) src = path.Clean("/" + src)
dst = path.Clean("/" + dst) dst = path.Clean("/" + dst)
realDest, err := files.GetRealPath(d.user.Scope, dst) realDest, _, err := files.GetRealPath(d.user.Scope, dst)
if err != nil { if err != nil {
return err return err
} }
realSrc, err := files.GetRealPath(d.user.Scope, src) realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
if err != nil { if err != nil {
return err return err
} }
file, err := files.FileInfoFaster(files.FileOptions{ file, err := files.FileInfoFaster(files.FileOptions{
Path: realSrc, Path: realSrc,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: false, ReadHeader: false,
@ -274,12 +273,13 @@ type DiskUsageResponse struct {
} }
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
file, err := files.FileInfoFaster(files.FileOptions{ file, err := files.FileInfoFaster(files.FileOptions{
Path: realPath, Path: realPath,
IsDir: isDir,
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: false, ReadHeader: false,

View File

@ -2,7 +2,6 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"reflect" "reflect"
@ -131,19 +130,21 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, errors.ErrEmptyPassword return http.StatusBadRequest, errors.ErrEmptyPassword
} }
newUser := users.ApplyDefaults(*req.Data)
userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root) userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
if err != nil { if err != nil {
log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
req.Data.Scope = userHome newUser.Scope = userHome
log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome) log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
_, err = files.GetRealPath(d.server.Root, req.Data.Scope) _, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
if err != nil { if err != nil {
fmt.Println("user path is not valid", req.Data.Scope) log.Println("user path is not valid", req.Data.Scope)
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
err = d.store.Users.Save(req.Data) err = d.store.Users.Save(&newUser)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -161,7 +162,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
if req.Data.ID != d.raw.(uint) { if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
_, err = files.GetRealPath(d.server.Root, req.Data.Scope) _, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
if err != nil { if err != nil {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
@ -175,7 +176,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
t := v.Type() t := v.Type()
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
field := t.Field(i) field := t.Field(i)
if field.Name != "Password" && field.Name != "Fs" { if field.Name == "Password" && req.Data.Password != "" {
req.Which = append(req.Which, field.Name)
} else if field.Name != "Password" && field.Name != "Fs" {
req.Which = append(req.Which, field.Name) req.Which = append(req.Which, field.Name)
} }
} }

View File

@ -9,7 +9,7 @@ checkExit() {
if command -v go &> /dev/null if command -v go &> /dev/null
then then
printf "\n == Running tests == \n" printf "\n == Running tests == \n"
go test -race -v ./... go test -race -parallel -v ./...
checkExit checkExit
else else
echo "ERROR: unable to perform tests" echo "ERROR: unable to perform tests"

View File

@ -20,8 +20,8 @@ type Runner struct {
// RunHook runs the hooks for the before and after event. // RunHook runs the hooks for the before and after event.
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error { func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
path, _ = files.GetRealPath(user.Scope, path) path, _, _ = files.GetRealPath(user.Scope, path)
dst, _ = files.GetRealPath(user.Scope, dst) dst, _, _ = files.GetRealPath(user.Scope, dst)
if r.Enabled { if r.Enabled {
if val, ok := r.Commands["before_"+evt]; ok { if val, ok := r.Commands["before_"+evt]; ok {

View File

@ -3,10 +3,9 @@ package settings
import ( import (
"log" "log"
"os" "os"
"strings" "path/filepath"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/gtsteffaniak/filebrowser/users"
) )
var Config Settings var Config Settings
@ -19,7 +18,16 @@ func Initialize(configFile string) {
log.Fatalf("Error unmarshaling YAML data: %v", err) log.Fatalf("Error unmarshaling YAML data: %v", err)
} }
Config.UserDefaults.Perm = Config.UserDefaults.Permissions Config.UserDefaults.Perm = Config.UserDefaults.Permissions
Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/") // Convert relative path to absolute path
realRoot, err := filepath.Abs(Config.Server.Root)
if err != nil {
log.Fatalf("Error getting root path: %v", err)
}
_, err = os.Stat(realRoot)
if err != nil {
log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
}
Config.Server.Root = realRoot
} }
func loadConfigFile(configFile string) []byte { func loadConfigFile(configFile string) []byte {
@ -77,8 +85,9 @@ func setDefaults() Settings {
HideDotfiles: true, HideDotfiles: true,
DarkMode: false, DarkMode: false,
DisableSettings: false, DisableSettings: false,
ViewMode: "normal",
Locale: "en", Locale: "en",
Permissions: users.Permissions{ Permissions: Permissions{
Create: false, Create: false,
Rename: false, Rename: false,
Modify: false, Modify: false,
@ -90,19 +99,3 @@ func setDefaults() Settings {
}, },
} }
} }
// Apply applies the default options to a user.
func (d *UserDefaults) Apply(u *users.User) {
u.StickySidebar = d.StickySidebar
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
}

View File

@ -2,7 +2,6 @@ package settings
import ( import (
"github.com/gtsteffaniak/filebrowser/rules" "github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/users"
) )
type Settings struct { type Settings struct {
@ -82,9 +81,20 @@ type UserDefaults struct {
By string `json:"by"` By string `json:"by"`
Asc bool `json:"asc"` Asc bool `json:"asc"`
} `json:"sorting"` } `json:"sorting"`
Perm users.Permissions `json:"perm"` Perm Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"` Permissions Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"` Commands []string `json:"commands,omitempty"`
HideDotfiles bool `json:"hideDotfiles"` HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"` DateFormat bool `json:"dateFormat"`
}
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
} }

View File

@ -1,7 +1,6 @@
package users package users
import ( import (
"log"
"sync" "sync"
"time" "time"
@ -123,7 +122,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
// Save saves the user in a storage. // Save saves the user in a storage.
func (s *Storage) Save(user *User) error { func (s *Storage) Save(user *User) error {
log.Println("Saving new user:", user.Username)
return s.back.Save(user) return s.back.Save(user)
} }

View File

@ -4,19 +4,9 @@ import (
"regexp" "regexp"
"github.com/gtsteffaniak/filebrowser/rules" "github.com/gtsteffaniak/filebrowser/rules"
"github.com/gtsteffaniak/filebrowser/settings"
) )
type Permissions struct {
Admin bool `json:"admin"`
Execute bool `json:"execute"`
Create bool `json:"create"`
Rename bool `json:"rename"`
Modify bool `json:"modify"`
Delete bool `json:"delete"`
Share bool `json:"share"`
Download bool `json:"download"`
}
// SortingSettings represents the sorting settings. // SortingSettings represents the sorting settings.
type Sorting struct { type Sorting struct {
By string `json:"by"` By string `json:"by"`
@ -25,24 +15,24 @@ type Sorting struct {
// User describes a user. // User describes a user.
type User struct { type User struct {
StickySidebar bool `json:"stickySidebar"` StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"` DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"` DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"` ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"` Username string `storm:"unique" json:"username"`
Password string `json:"password"` Password string `json:"password"`
Scope string `json:"scope"` Scope string `json:"scope"`
Locale string `json:"locale"` Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"` LockPassword bool `json:"lockPassword"`
ViewMode string `json:"viewMode"` ViewMode string `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"` Perm settings.Permissions `json:"perm"`
Commands []string `json:"commands"` Commands []string `json:"commands"`
Sorting Sorting `json:"sorting"` Sorting Sorting `json:"sorting"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"` HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"` DateFormat bool `json:"dateFormat"`
GallerySize int `json:"gallerySize"` GallerySize int `json:"gallerySize"`
} }
var PublicUser = User{ var PublicUser = User{
@ -51,7 +41,7 @@ var PublicUser = User{
Scope: "./", Scope: "./",
ViewMode: "normal", ViewMode: "normal",
LockPassword: true, LockPassword: true,
Perm: Permissions{ Perm: settings.Permissions{
Create: false, Create: false,
Rename: false, Rename: false,
Modify: false, Modify: false,
@ -81,3 +71,20 @@ func (u *User) CanExecute(command string) bool {
return false return false
} }
// Apply applies the default options to a user.
func ApplyDefaults(u User) User {
u.StickySidebar = settings.Config.UserDefaults.StickySidebar
u.DisableSettings = settings.Config.UserDefaults.DisableSettings
u.DarkMode = settings.Config.UserDefaults.DarkMode
u.Scope = settings.Config.UserDefaults.Scope
u.Locale = settings.Config.UserDefaults.Locale
u.ViewMode = settings.Config.UserDefaults.ViewMode
u.SingleClick = settings.Config.UserDefaults.SingleClick
u.Perm = settings.Config.UserDefaults.Perm
u.Sorting = settings.Config.UserDefaults.Sorting
u.Commands = settings.Config.UserDefaults.Commands
u.HideDotfiles = settings.Config.UserDefaults.HideDotfiles
u.DateFormat = settings.Config.UserDefaults.DateFormat
return u
}

View File

@ -9,7 +9,7 @@
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script> <script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
[{[ end ]}] [{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title> <title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]FileBrowser Quantum[{[ end ]}]</title>
<link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png"> <link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
@ -33,8 +33,8 @@
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL; var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = { var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser', "name": window.FileBrowser.Name || 'FileBrowser Quantum',
"short_name": window.FileBrowser.Name || 'File Browser', "short_name": window.FileBrowser.Name || 'FileBrowser',
"icons": [ "icons": [
{ {
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png", "src": fullStaticURL + "/img/icons/android-chrome-256x256.png",

View File

@ -1,6 +1,6 @@
{ {
"name": "File Browser", "name": "FileBrowser",
"short_name": "File Browser", "short_name": "FileBrowser",
"icons": [ "icons": [
{ {
"src": "./img/icons/android-chrome-192x192.png", "src": "./img/icons/android-chrome-192x192.png",

View File

@ -1,122 +1,151 @@
import { createURL, fetchURL, removePrefix } from "./utils"; import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import { state } from "@/store"; import { state } from "@/store";
import { notify } from "@/notify";
export async function fetch(url,content=false) { // Notify if errors occur
url = removePrefix(url); export async function fetch(url, content = false) {
try {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}?content=${content}`, {}); const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
const data = await res.json();
data.url = `/files${url}`;
let data = await res.json(); if (data.isDir) {
data.url = `/files${url}`; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (data.isDir) { if (item.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; item.url += "/";
data.items = data.items.map((item, index) => { }
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) { return item;
item.url += "/"; });
} }
return item; return data;
}); } catch (err) {
notify.showError(err.message || "Error fetching data");
throw err;
} }
return data;
} }
async function resourceAction(url, method, content) { async function resourceAction(url, method, content) {
url = removePrefix(url); try {
url = removePrefix(url);
let opts = { method }; let opts = { method };
if (content) { if (content) {
opts.body = content; opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
} catch (err) {
notify.showError(err.message || "Error performing resource action");
throw err;
} }
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
} }
export async function remove(url) { export async function remove(url) {
return resourceAction(url, "DELETE"); try {
return await resourceAction(url, "DELETE");
} catch (err) {
notify.showError(err.message || "Error deleting resource");
throw err;
}
} }
export async function put(url, content = "") { export async function put(url, content = "") {
return resourceAction(url, "PUT", content); try {
return await resourceAction(url, "PUT", content);
} catch (err) {
notify.showError(err.message || "Error putting resource");
throw err;
}
} }
export function download(format, ...files) { export function download(format, ...files) {
let url = `${baseURL}/api/raw`; try {
let url = `${baseURL}/api/raw`;
if (files.length === 1) { if (files.length === 1) {
url += removePrefix(files[0]) + "?"; url += removePrefix(files[0]) + "?";
} else { } else {
let arg = ""; let arg = "";
for (let file of files) { for (let file of files) {
arg += removePrefix(file) + ","; arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
} }
arg = arg.substring(0, arg.length - 1); if (format) {
arg = encodeURIComponent(arg); url += `algo=${format}&`;
url += `/?files=${arg}&`; }
}
if (format) { if (state.jwt) {
url += `algo=${format}&`; url += `auth=${state.jwt}&`;
} }
if (state.jwt) { window.open(url);
url += `auth=${state.jwt}&`; } catch (err) {
notify.showError(err.message || "Error downloading files");
} }
window.open(url);
} }
export async function post(url, content = "", overwrite = false, onupload) { export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url); try {
url = removePrefix(url);
let bufferContent; let bufferContent;
if ( if (
content instanceof Blob && content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol) !["http:", "https:"].includes(window.location.protocol)
) { ) {
bufferContent = await new Response(content).arrayBuffer(); bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
} }
request.onload = () => { return new Promise((resolve, reject) => {
if (request.status === 200) { let request = new XMLHttpRequest();
resolve(request.responseText); request.open(
} else if (request.status === 409) { "POST",
reject(request.status); `${baseURL}/api/resources${url}?override=${overwrite}`,
} else { true
reject(request.responseText); );
request.setRequestHeader("X-Auth", state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
} }
};
request.onerror = () => { request.onload = () => {
reject(new Error("001 Connection aborted")); if (request.status === 200) {
}; resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.send(bufferContent || content); request.onerror = () => {
}); reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
} catch (err) {
notify.showError(err.message || "Error posting resource");
throw err;
}
} }
function moveCopy(items, copy = false, overwrite = false, rename = false) { function moveCopy(items, copy = false, overwrite = false, rename = false) {
@ -131,7 +160,10 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
promises.push(resourceAction(url, "PATCH")); promises.push(resourceAction(url, "PATCH"));
} }
return Promise.all(promises); return Promise.all(promises).catch((err) => {
notify.showError(err.message || "Error moving/copying resources");
throw err;
});
} }
export function move(items, overwrite = false, rename = false) { export function move(items, overwrite = false, rename = false) {
@ -143,44 +175,68 @@ export function copy(items, overwrite = false, rename = false) {
} }
export async function checksum(url, algo) { export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); try {
return (await data.json()).checksums[algo]; const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
} catch (err) {
notify.showError(err.message || "Error fetching checksum");
throw err;
}
} }
export function getDownloadURL(file, inline) { export function getDownloadURL(file, inline) {
const params = { try {
...(inline && { inline: "true" }), const params = {
}; ...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params); return createURL("api/raw" + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting download URL");
throw err;
}
} }
export function getPreviewURL(file, size) { export function getPreviewURL(file, size) {
const params = { try {
inline: "true", const params = {
key: Date.parse(file.modified), inline: "true",
}; key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params); return createURL("api/preview/" + size + file.path, params);
} catch (err) {
notify.showError(err.message || "Error getting preview URL");
throw err;
}
} }
export function getSubtitlesURL(file) { export function getSubtitlesURL(file) {
const params = { try {
inline: "true", const params = {
}; inline: "true",
};
const subtitles = []; const subtitles = [];
for (const sub of file.subtitles) { for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params)); subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
} catch (err) {
notify.showError(err.message || "Error fetching subtitles URL");
throw err;
} }
return subtitles;
} }
export async function usage(url) { export async function usage(url) {
url = removePrefix(url); try {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {}); const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
return await res.json(); } catch (err) {
notify.showError(err.message || "Error fetching usage data");
throw err;
}
} }

View File

@ -84,5 +84,6 @@ export function getDownloadURL(share, inline = false) {
if (share.path == undefined) { if (share.path == undefined) {
share.path = "" share.path = ""
} }
return createURL("api/public/dl/" + share.hash + "/"+share.path, params, false); const path = share.path.replace("/share/"+share.hash +"/","")
return createURL("api/public/dl/" + share.hash + "/"+path, params, false);
} }

View File

@ -1,22 +1,28 @@
import { fetchURL, removePrefix } from "./utils"; import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url"; import url from "../utils/url";
import { notify } from "@/notify"; // Import notify for error handling
export default async function search(base, query) { export default async function search(base, query) {
base = removePrefix(base); try {
query = encodeURIComponent(query); base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) { if (!base.endsWith("/")) {
base += "/"; base += "/";
}
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
return item;
});
return data;
} catch (err) {
notify.showError(err.message || "Error occurred during search");
throw err;
} }
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
return item;
});
return data;
} }

View File

@ -1,47 +1,76 @@
import { fetchURL, fetchJSON } from "@/api/utils"; import { fetchURL, fetchJSON } from "@/api/utils";
import { notify } from "@/notify"; // Import notify for error handling
export async function getAllUsers() { export async function getAllUsers() {
return await fetchJSON(`/api/users`, {}); try {
return await fetchJSON(`/api/users`, {});
} catch (err) {
notify.showError(err.message || "Failed to fetch users");
throw err; // Re-throw to handle further if needed
}
} }
export async function get(id) { export async function get(id) {
return fetchJSON(`/api/users/${id}`, {}); try {
return await fetchJSON(`/api/users/${id}`, {});
} catch (err) {
notify.showError(err.message || `Failed to fetch user with ID: ${id}`);
throw err;
}
} }
export async function create(user) { export async function create(user) {
const res = await fetchURL(`/api/users`, { try {
method: "POST", const res = await fetchURL(`/api/users`, {
body: JSON.stringify({ method: "POST",
what: "user", body: JSON.stringify({
which: [], what: "user",
data: user, which: [],
}), data: user,
}); }),
});
if (res.status === 201) { if (res.status === 201) {
return res.headers.get("Location"); return res.headers.get("Location");
} else {
throw new Error("Failed to create user");
}
} catch (err) {
notify.showError(err.message || "Error creating user");
throw err;
} }
} }
export async function update(user, which = ["all"]) { export async function update(user, which = ["all"]) {
if (which[0] != "password") { try {
user.password = ""; // List of keys to exclude from the "which" array
const excludeKeys = ["id", "name"];
// Filter out the keys from "which"
which = which.filter(item => !excludeKeys.includes(item));
if (user.username === "publicUser") {
return;
}
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
} catch (err) {
notify.showError(err.message || `Failed to update user with ID: ${user.id}`);
throw err;
} }
if (user.username == "publicUser") {
return
}
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
} }
export async function remove(id) { export async function remove(id) {
await fetchURL(`/api/users/${id}`, { try {
method: "DELETE", await fetchURL(`/api/users/${id}`, {
}); method: "DELETE",
});
} catch (err) {
notify.showError(err.message || `Failed to delete user with ID: ${id}`);
throw err;
}
} }

View File

@ -2,7 +2,7 @@ import { state } from "@/store";
import { renew, logout } from "@/utils/auth"; import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url"; import { encodePath } from "@/utils/url";
import { showError } from "@/notify"; import { notify } from "@/notify";
export async function fetchURL(url, opts, auth = true) { export async function fetchURL(url, opts, auth = true) {
opts = opts || {}; opts = opts || {};
@ -51,7 +51,7 @@ export async function fetchJSON(url, opts) {
if (res.status === 200) { if (res.status === 200) {
return res.json(); return res.json();
} else { } else {
showError("unable to fetch : " + url + "status" + res.status); notify.showError("unable to fetch : " + url + "status" + res.status);
throw new Error(res.status); throw new Error(res.status);
} }
} }

View File

@ -14,7 +14,7 @@
<component :is="element" :to="link.url">{{ link.name }}</component> <component :is="element" :to="link.url">{{ link.name }}</component>
</span> </span>
<action style="display: contents" v-if="showShare" icon="share" show="share" /> <action style="display: contents" v-if="showShare" icon="share" show="share" />
<div v-if="isResizableView"> <div v-if="isCardView">
Size: Size:
<input <input
v-model="gallerySize" v-model="gallerySize"
@ -31,7 +31,7 @@
<script> <script>
import { state, mutations, getters } from "@/store"; // Import mutations as well import { state, mutations, getters } from "@/store"; // Import mutations as well
import Action from "@/components/header/Action.vue"; import Action from "@/components/Action.vue";
export default { export default {
name: "breadcrumbs", name: "breadcrumbs",
@ -51,8 +51,8 @@ export default {
}, },
props: ["base", "noLink"], props: ["base", "noLink"],
computed: { computed: {
isResizableView() { isCardView() {
return getters.isResizableView(); return getters.isCardView();
}, },
items() { items() {
const relativePath = state.route.path.replace(this.base, ""); const relativePath = state.route.path.replace(this.base, "");
@ -107,11 +107,6 @@ export default {
return state.user?.perm && state.user?.perm.share; // Access from state directly return state.user?.perm && state.user?.perm.share; // Access from state directly
}, },
}, },
methods: { methods: { },
// Example of a method using mutations
updateUserPermissions(newPerms) {
mutations.updateUser({ perm: newPerms });
},
},
}; };
</script> </script>

View File

@ -0,0 +1,230 @@
<template>
<div
id="context-menu"
ref="contextMenu"
v-show="showContext"
:style="{
top: `${top}px`,
left: `${left}px`,
}"
class="button"
:class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} selected</span>
</div>
<action
v-if="!headerButtons.select"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="!headerButtons.select"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="!headerButtons.select"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="!isMultiple"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</template>
<script>
import downloadFiles from "@/utils/download";
import { state, getters, mutations } from "@/store"; // Import your custom store
import Action from "@/components/Action.vue";
export default {
name: "ContextMenu",
components: {
Action,
},
data() {
return {
posX: 0,
posY: 0,
};
},
computed: {
isMultiple() {
return state.multiple;
},
user() {
return state.user;
},
isMobile() {
return getters.isMobile();
},
showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
this.setPositions();
return true;
}
return false;
},
top() {
// Ensure the context menu stays within the viewport
return Math.min(
this.posY,
window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
);
},
left() {
return Math.min(
this.posX,
window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
);
},
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
uploadFunc() {
mutations.showHover("upload");
},
showHover(value) {
return mutations.showHover(value);
},
setPositions() {
const contextProps = getters.currentPrompt().props;
this.posX = contextProps.posX;
this.posY = contextProps.posY;
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
startDownload() {
downloadFiles();
},
},
};
</script>
<style scoped>
#context-menu {
position: absolute;
z-index: 1000;
background-color: white;
max-width: 20em;
min-width: 15em;
min-height: 4em;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
#context-menu.mobile {
top: 50% !important;
left: 50% !important;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.selected-count-header {
border-radius: 0.5em;
cursor: unset;
margin-bottom: 0.5em;
}
#context-menu .action {
width: auto;
display: flex;
align-items: center;
}
#context-menu > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#context-menu .action span {
display: none;
}
/* File selection */
#context-menu.dark-mode {
background: var(--surfaceSecondary) !important;
}
#context-menu.dark-mode span {
color: var(--textPrimary) !important;
}
</style>

View File

@ -1,168 +0,0 @@
<template>
<div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
<span>{{ selectedCount }} selected</span>
<div>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="headerButtons.select"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</div>
</template>
<script>
import { state, getters, mutations } from "@/store"; // Import your custom store
import { files as api } from "@/api";
import Action from "@/components/header/Action.vue";
export default {
name: "fileSelection",
components: {
Action,
},
computed: {
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
download() {
if (getters.isSingleFileSelected()) {
api.download(null, getters.selectedDownloadUrl());
return;
}
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
try {
api.download(format, ...files);
showSuccess("download started");
} catch (e) {
showError("error downloading", e);
}
},
});
},
},
};
</script>
<style>
@media (min-width: 800px) {
#file-selection {
bottom: 4em;
}
}
#file-selection .action {
border-radius: 50%;
width: auto;
}
#file-selection > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#file-selection .action span {
display: none;
}
/* File Selection */
#file-selection {
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
position: fixed;
bottom: 4em;
left: 50%;
transform: translateX(-50%);
align-items: center;
background: #fff;
max-width: 30em;
z-index: 3;
border-radius: 1em;
display: flex;
width: 90%;
}
/* File selection */
#file-selection.dark-mode {
background: var(--surfaceSecondary) !important;
}
#file-selection.dark-mode span {
color: var(--textPrimary) !important;
}
</style>

View File

@ -16,6 +16,7 @@
<i v-else class="material-icons">search</i> <i v-else class="material-icons">search</i>
<!-- Input field for search --> <!-- Input field for search -->
<input <input
id="main-input"
class="main-input" class="main-input"
type="text" type="text"
@keyup.exact="keyup" @keyup.exact="keyup"
@ -194,7 +195,6 @@
import ButtonGroup from "./ButtonGroup.vue"; import ButtonGroup from "./ButtonGroup.vue";
import { search } from "@/api"; import { search } from "@/api";
import { getters, mutations, state } from "@/store"; import { getters, mutations, state } from "@/store";
import { showError } from "@/notify";
var boxes = { var boxes = {
folder: { label: "folders", icon: "folder" }, folder: { label: "folders", icon: "folder" },
@ -248,13 +248,18 @@ export default {
this.submit(); this.submit();
}, },
active(active) { active(active) {
// this is hear to allow for animation
const resultList = document.getElementById("result-list"); const resultList = document.getElementById("result-list");
if (!active) { if (!active) {
resultList.classList.remove("active"); resultList.classList.remove("active");
this.value = "";
event.stopPropagation();
mutations.closeHovers();
return; return;
} }
setTimeout(() => { setTimeout(() => {
resultList.classList.add("active"); resultList.classList.add("active");
document.getElementById("main-input").focus();
}, 100); }, 100);
}, },
value() { value() {
@ -394,11 +399,9 @@ export default {
} }
let path = state.route.path; let path = state.route.path;
this.ongoing = true; this.ongoing = true;
try {
this.results = await search(path, searchTypesFull + this.value); this.results = await search(path, searchTypesFull + this.value);
} catch (error) {
showError(error);
}
this.ongoing = false; this.ongoing = false;
if (this.results.length == 0) { if (this.results.length == 0) {
this.noneMessage = "No results found in indexed search."; this.noneMessage = "No results found in indexed search.";

View File

@ -24,9 +24,9 @@
</template> </template>
<script> <script>
import { state, mutations, getters } from "@/store"; import { state, mutations } from "@/store";
import throttle from "@/utils/throttle"; import throttle from "@/utils/throttle";
import { showError } from "@/notify"; import { notify } from "@/notify";
export default { export default {
props: { props: {
src: String, src: String,
@ -131,8 +131,7 @@ export default {
imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
} }
} catch (error) { } catch (error) {
showError("Error decoding TIFF"); notify.showError("Error decoding TIFF");
console.error("Error decoding TIFF:", error);
} }
}, },
onMouseUp() { onMouseUp() {

View File

@ -1,37 +1,38 @@
<template> <template>
<div <component
:class="{ activebutton: this.isMaximized && this.isSelected }" :is="isSelected || user.singleClick ? 'a' : 'div'"
class="item" :href="isSelected || user.singleClick ? url : undefined"
:class="{
item: true,
activebutton: isMaximized && isSelected,
}"
role="button" role="button"
tabindex="0" tabindex="0"
:draggable="isDraggable" :draggable="isDraggable"
@dragstart="dragStart" @dragstart="dragStart"
@dragover="dragOver" @dragover="dragOver"
@drop="drop" @drop="drop"
@click="itemClick"
:data-dir="isDir" :data-dir="isDir"
:data-type="type" :data-type="type"
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
@click="isSelected || user.singleClick ? toggleClick() : itemClick($event)"
> >
<div <div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
@click="toggleClick"
:class="{ activetitle: this.isMaximized && this.isSelected }"
>
<img <img
v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView" v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
:class="{ activeimg: this.isMaximized && this.isSelected }" :class="{ activeimg: isMaximized && isSelected }"
ref="thumbnail" ref="thumbnail"
/> />
<i <i
:class="{ iconActive: this.isMaximized && this.isSelected }" :class="{ iconActive: isMaximized && isSelected }"
v-else v-else
class="material-icons" class="material-icons"
></i> ></i>
</div> </div>
<div class="text" :class="{ activecontent: this.isMaximized && this.isSelected }"> <div class="text" :class="{ activecontent: isMaximized && isSelected }">
<p class="name">{{ name }}</p> <p class="name">{{ name }}</p>
<p v-if="isDir" class="size" data-order="-1">&mdash;</p> <p v-if="isDir" class="size" data-order="-1">&mdash;</p>
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p> <p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
@ -39,7 +40,7 @@
<time :datetime="modified">{{ humanTime() }}</time> <time :datetime="modified">{{ humanTime() }}</time>
</p> </p>
</div> </div>
</div> </component>
</template> </template>
<style> <style>
@ -73,7 +74,7 @@ import { state, getters, mutations } from "@/store"; // Import your custom store
export default { export default {
name: "item", name: "item",
data: function () { data() {
return { return {
isThumbnailInView: false, isThumbnailInView: false,
isMaximized: false, isMaximized: false,
@ -98,27 +99,12 @@ export default {
selected() { selected() {
return state.selected; return state.selected;
}, },
req() {
return state.req;
},
jwt() {
return state.jwt;
},
selectedCount() {
return getters.selectedCount();
},
isClicked() { isClicked() {
if (state.user.singleClick || !this.allowedView) { if (state.user.singleClick || !this.allowedView) {
return false; return false;
} }
return !this.isMaximized; return !this.isMaximized;
}, },
allowedView() {
return state.user.viewMode != "gallery" && state.user.viewMode != "normal";
},
singleClick() {
return this.readOnly == undefined && state.user.singleClick;
},
isSelected() { isSelected() {
return this.selected.indexOf(this.index) !== -1; return this.selected.indexOf(this.index) !== -1;
}, },
@ -181,18 +167,18 @@ export default {
toggleClick() { toggleClick() {
this.isMaximized = this.isClicked; this.isMaximized = this.isClicked;
}, },
humanSize: function () { humanSize() {
return this.type == "invalid_link" return this.type == "invalid_link"
? "invalid link" ? "invalid link"
: getHumanReadableFilesize(this.size); : getHumanReadableFilesize(this.size);
}, },
humanTime: function () { humanTime() {
if (this.readOnly == undefined && state.user.dateFormat) { if (this.readOnly == undefined && state.user.dateFormat) {
return fromNow(this.modified, state.user.locale).format("L LT"); return fromNow(this.modified, state.user.locale).format("L LT");
} }
return fromNow(this.modified, state.user.locale); return fromNow(this.modified, state.user.locale);
}, },
dragStart: function () { dragStart() {
if (getters.selectedCount() === 0) { if (getters.selectedCount() === 0) {
mutations.addSelected(this.index); mutations.addSelected(this.index);
return; return;
@ -203,7 +189,7 @@ export default {
mutations.addSelected(this.index); mutations.addSelected(this.index);
} }
}, },
dragOver: function (event) { dragOver(event) {
if (!this.canDrop) return; if (!this.canDrop) return;
event.preventDefault(); event.preventDefault();
@ -217,7 +203,7 @@ export default {
el.style.opacity = 1; el.style.opacity = 1;
}, },
drop: async function (event) { async drop(event) {
if (!this.canDrop) return; if (!this.canDrop) return;
event.preventDefault(); event.preventDefault();
@ -276,11 +262,11 @@ export default {
action(overwrite, rename); action(overwrite, rename);
}, },
itemClick: function (event) { itemClick(event) {
if (this.singleClick && !state.multiple) this.open(); if (this.singleClick && !state.multiple) this.open();
else this.click(event); else this.click(event);
}, },
click: function (event) { click(event) {
if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault(); if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
setTimeout(() => { setTimeout(() => {
@ -321,7 +307,7 @@ export default {
mutations.resetSelected(); mutations.resetSelected();
mutations.addSelected(this.index); mutations.addSelected(this.index);
}, },
open: function () { open() {
this.$router.push({ path: this.url }); this.$router.push({ path: this.url });
}, },
}, },

View File

@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { showError } from "@/notify"; import { notify } from "@/notify";
export default { export default {
name: "copy", name: "copy",
@ -102,7 +102,7 @@ export default {
}) })
.catch((e) => { .catch((e) => {
buttons.done("copy"); buttons.done("copy");
showError(e); notify.showError(e);
}); });
}; };

View File

@ -33,7 +33,7 @@
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
import { showError,showSuccess } from "@/notify"; import { notify } from "@/notify";
export default { export default {
name: "delete", name: "delete",
@ -59,7 +59,7 @@ export default {
if (!this.isListing) { if (!this.isListing) {
await api.remove(state.route.path); await api.remove(state.route.path);
buttons.success("delete"); buttons.success("delete");
showSuccess("Deleted item successfully") showSuccess("Deleted item successfully");
this.currentPrompt?.confirm(); this.currentPrompt?.confirm();
this.closeHovers(); this.closeHovers();
@ -79,11 +79,11 @@ export default {
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
showSuccess("Deleted item successfully") showSuccess("Deleted item successfully");
mutations.setReload(true); // Handle reload as needed mutations.setReload(true); // Handle reload as needed
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
showError(e); notify.showError(e);
if (this.isListing) mutations.setReload(true); // Handle reload as needed if (this.isListing) mutations.setReload(true); // Handle reload as needed
} }
}, },

View File

@ -21,7 +21,7 @@
</template> </template>
<script> <script>
import { users as api } from "@/api"; import { users as api } from "@/api";
import { showSuccess,showError } from "@/notify"; import { notify } from "@/notify";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
@ -40,12 +40,12 @@ export default {
event.preventDefault(); event.preventDefault();
try { try {
await api.remove(this.user.id); await api.remove(this.user.id);
this.$router.push({ path: "/settings/users" }); this.$router.push({ path: "/settings",hash:"#users-main" });
showSuccess(this.$t("settings.userDeleted")); notify.showSuccess(this.$t("settings.userDeleted"));
} catch (e) { } catch (e) {
e.message === "403" e.message === "403"
? showError(this.$t("errors.forbidden"), false) ? notify.showError(this.$t("errors.forbidden"), false)
: showError(e); : notify.showError(e);
} }
}, },
closeHovers() { closeHovers() {
@ -80,7 +80,7 @@ export default {
mutations.setReload(true); // Handle reload as needed mutations.setReload(true); // Handle reload as needed
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
showError(e); notify.showError(e);
if (this.isListing) mutations.setReload(true); // Handle reload as needed if (this.isListing) mutations.setReload(true); // Handle reload as needed
} }
}, },

View File

@ -24,7 +24,6 @@
import { state, mutations } from "@/store"; import { state, mutations } from "@/store";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { showError } from "@/notify";
export default { export default {
name: "file-list", name: "file-list",
@ -86,7 +85,7 @@ export default {
// content. // content.
let uri = event.currentTarget.dataset.url; let uri = event.currentTarget.dataset.url;
files.fetch(uri).then(this.fillOptions).catch(showError); files.fetch(uri).then(this.fillOptions);
}, },
touchstart(event) { touchstart(event) {
let url = event.currentTarget.dataset.url; let url = event.currentTarget.dataset.url;

View File

@ -75,7 +75,6 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
import { formatTimestamp } from "@/utils/moment"; import { formatTimestamp } from "@/utils/moment";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { state, getters, mutations } from "@/store"; // Import your custom store import { state, getters, mutations } from "@/store"; // Import your custom store
import { showError } from "@/notify";
export default { export default {
name: "info", name: "info",
@ -146,12 +145,8 @@ export default {
link = state.route.path; link = state.route.path;
} }
try { const hash = await api.checksum(link, algo);
const hash = await api.checksum(link, algo); event.target.innerHTML = hash;
event.target.innerHTML = hash;
} catch (e) {
showError(e);
}
}, },
}, },
}; };

View File

@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { showError } from "@/notify"; import { notify } from "@/notify";
export default { export default {
name: "move", name: "move",
@ -95,7 +95,7 @@ export default {
}) })
.catch((e) => { .catch((e) => {
buttons.done("move"); buttons.done("move");
showError(e); notify.showError(e);
}); });
}; };
@ -121,7 +121,6 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
}, },
}, },
}; };

View File

@ -39,7 +39,6 @@
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import { getters, mutations, state } from "@/store"; // Import your custom store import { getters, mutations, state } from "@/store"; // Import your custom store
import { showError } from "@/notify";
export default { export default {
name: "new-dir", name: "new-dir",
@ -87,16 +86,12 @@ export default {
uri += encodeURIComponent(this.name) + "/"; uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { await api.post(uri);
await api.post(uri); if (this.redirect) {
if (this.redirect) { this.$router.push({ path: uri });
this.$router.push({ path: uri }); } else if (!this.base) {
} else if (!this.base) { const res = await api.fetch(url.removeLastDir(uri) + "/");
const res = await api.fetch(url.removeLastDir(uri) + "/"); mutations.updateRequest(res);
mutations.updateRequest(res);
}
} catch (e) {
showError(e);
} }
mutations.closeHovers(); mutations.closeHovers();

View File

@ -73,12 +73,8 @@ export default {
uri += encodeURIComponent(this.name); uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { await api.post(uri);
await api.post(uri); this.$router.push({ path: uri });
this.$router.push({ path: uri });
} catch (e) {
showError(e);
}
mutations.closeHovers(); mutations.closeHovers();
}, },

View File

@ -98,18 +98,14 @@ export default {
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name); newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try { await api.move([{ from: oldLink, to: newLink }]);
await api.move([{ from: oldLink, to: newLink }]); if (!this.isListing) {
if (!this.isListing) { this.$router.push({ path: newLink });
this.$router.push({ path: newLink }); return;
return;
}
mutations.setReload(true);
} catch (e) {
showError(e);
} }
mutations.setReload(true);
mutations.closeHovers(); mutations.closeHovers();
}, },
}, },

View File

@ -120,7 +120,7 @@
</div> </div>
</template> </template>
<script> <script>
import { showSuccess, showError } from "@/notify"; import { notify } from "@/notify";
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
import { share as api, pub as pub_api } from "@/api"; import { share as api, pub as pub_api } from "@/api";
import { fromNow } from "@/utils/moment"; import { fromNow } from "@/utils/moment";
@ -173,22 +173,18 @@ export default {
}, },
}, },
async beforeMount() { async beforeMount() {
try { const links = await api.get(this.url);
const links = await api.get(this.url); this.links = links;
this.links = links; this.sort();
this.sort();
if (this.links.length === 0) { if (this.links.length === 0) {
this.listing = false; this.listing = false;
}
} catch (e) {
showError(e);
} }
}, },
mounted() { mounted() {
this.clip = new Clipboard(".copy-clipboard"); this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => { this.clip.on("success", () => {
showSuccess(this.$t("success.linkCopied")); notify.showSuccess(this.$t("success.linkCopied"));
}); });
}, },
beforeUnmount() { beforeUnmount() {
@ -198,38 +194,30 @@ export default {
async submit() { async submit() {
let isPermanent = !this.time || this.time === 0; let isPermanent = !this.time || this.time === 0;
try { let res = null;
let res = null;
if (isPermanent) { if (isPermanent) {
res = await api.create(this.url, this.password); res = await api.create(this.url, this.password);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(this.url, this.password, this.time, this.unit);
}
this.links.push(res);
this.sort();
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
showError(e);
} }
this.links.push(res);
this.sort();
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true;
}, },
async deleteLink(event, link) { async deleteLink(event, link) {
event.preventDefault(); event.preventDefault();
try { await api.remove(link.hash);
await api.remove(link.hash); this.links = this.links.filter((item) => item.hash !== link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length === 0) { if (this.links.length === 0) {
this.listing = false; this.listing = false;
}
} catch (e) {
showError(e);
} }
}, },
humanTime(time) { humanTime(time) {

View File

@ -68,7 +68,7 @@ export default {
handleFiles(event); handleFiles(event);
}; };
const handleFiles = (event) => { const handleFiles = async (event) => {
mutations.closeHovers(); mutations.closeHovers();
const files = event.target.files; const files = event.target.files;
if (!files) return; if (!files) return;
@ -94,21 +94,20 @@ export default {
if (conflict) { if (conflict) {
mutations.showHover({ mutations.showHover({
name: "replace", name: "replace",
action: (event) => { action: async (event) => {
event.preventDefault(); event.preventDefault();
mutations.closeHovers(); mutations.closeHovers();
upload.handleFiles(uploadFiles, path, false); await upload.handleFiles(uploadFiles, path, false);
}, },
confirm: (event) => { confirm: async (event) => {
event.preventDefault(); event.preventDefault();
mutations.closeHovers(); mutations.closeHovers();
upload.handleFiles(uploadFiles, path, true); await upload.handleFiles(uploadFiles, path, true);
}, },
}); });
return; } else {
await upload.handleFiles(uploadFiles, path, true);
} }
upload.handleFiles(uploadFiles, path, true);
mutations.setReload(true); mutations.setReload(true);
}; };

View File

@ -7,6 +7,7 @@
type="text" type="text"
v-model="user.username" v-model="user.username"
id="username" id="username"
@input="emitUpdate"
/> />
</p> </p>
@ -18,22 +19,24 @@
:placeholder="passwordPlaceholder" :placeholder="passwordPlaceholder"
v-model="user.password" v-model="user.password"
id="password" id="password"
@input="emitUpdate"
/> />
</p> </p>
<p> <p>
<label for="scope">{{ $t("settings.scope") }}</label> <label for="scope">{{ $t("settings.scope") }}</label>
<input <input
:disabled="createUserDirData" :disabled="createUserDir"
:placeholder="scopePlaceholder" :placeholder="scopePlaceholder"
class="input input--block" class="input input--block"
type="text" type="text"
v-model="user.scope" v-model="user.scope"
id="scope" id="scope"
@input="emitUpdate"
/> />
</p> </p>
<p class="small" v-if="displayHomeDirectoryCheckbox"> <p class="small" v-if="displayHomeDirectoryCheckbox">
<input type="checkbox" v-model="createUserDirData" /> <input type="checkbox" v-model="createUserDir" />
{{ $t("settings.createUserHomeDirectory") }} {{ $t("settings.createUserHomeDirectory") }}
</p> </p>
@ -43,31 +46,32 @@
class="input input--block" class="input input--block"
id="locale" id="locale"
v-model:locale="user.locale" v-model:locale="user.locale"
@input="emitUpdate"
></languages> ></languages>
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<input <input
type="checkbox" type="checkbox"
:disabled="user.perm.admin" :disabled="user.perm?.admin"
v-model="user.lockPassword" v-model="user.lockPassword"
@input="emitUpdate"
/> />
{{ $t("settings.lockPassword") }} {{ $t("settings.lockPassword") }}
</p> </p>
<permissions :perm="user.perm" /> <permissions :perm="localUser.perm" />
<commands v-if="isExecEnabled" v-model:commands="user.commands" /> <commands v-if="isExecEnabled" v-model:commands="user.commands" />
<div v-if="!isDefault"> <div v-if="!isDefault">
<h3>{{ $t("settings.rules") }}</h3> <h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p> <p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules v-model:rules="user.rules" /> <rules v-model:rules="user.rules" @input="emitUpdate" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { state } from "@/store"
import Languages from "./Languages.vue"; import Languages from "./Languages.vue";
import Rules from "./Rules.vue"; import Rules from "./Rules.vue";
import Permissions from "./Permissions.vue"; import Permissions from "./Permissions.vue";
@ -75,35 +79,51 @@ import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default { export default {
name: "user", name: "UserForm",
data() {
return {
createUserDirData: false,
originalUserScope: "/",
};
},
components: { components: {
Permissions, Permissions,
Languages, Languages,
Rules, Rules,
Commands, Commands,
}, },
props: [ "createUserDir", "isNew", "isDefault"], data() {
created() { return {
this.originalUserScope = state.user.scope; createUserDir: false,
this.createUserDirData = this.createUserDir; originalUserScope: ".",
localUser: { ...this.user },
};
},
props: {
user: Object, // Define user as a prop
isDefault: Boolean,
isNew: Boolean,
},
watch: {
user: {
handler(newUser) {
console.log("UserForm: user changed", newUser);
this.localUser = { ...newUser }; // Watch for changes in the parent and update the local copy
},
immediate: true,
deep: true,
},
"user.perm.admin": function (newValue) {
if (newValue) {
this.user.lockPassword = false;
}
},
createUserDir(newVal) {
this.user.scope = newVal ? "" : this.originalUserScope;
},
}, },
computed: { computed: {
user() {
return state.user;
},
passwordPlaceholder() { passwordPlaceholder() {
return this.isNew ? "" : this.$t("settings.avoidChanges"); return this.isNew ? "" : this.$t("settings.avoidChanges");
}, },
scopePlaceholder() { scopePlaceholder() {
return this.createUserDir return this.createUserDir
? this.$t("settings.userScopeGenerationPlaceholder") ? this.$t("settings.userScopeGenerationPlaceholder")
: ""; : "./";
}, },
displayHomeDirectoryCheckbox() { displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir; return this.isNew && this.createUserDir;
@ -112,10 +132,5 @@ export default {
return enableExec; // Removed arrow function return enableExec; // Removed arrow function
}, },
}, },
watch: {
createUserDirData(newVal) {
state.user.scope = newVal ? "" : this.originalUserScope;
},
},
}; };
</script> </script>

View File

@ -44,35 +44,6 @@
<!-- Section for logged-in users --> <!-- Section for logged-in users -->
<div v-if="isLoggedIn" class="sidebar-scroll-list"> <div v-if="isLoggedIn" class="sidebar-scroll-list">
<!-- Buttons visible if user has create permission -->
<div v-if="user.perm?.create">
<!-- New Folder button -->
<button
@click="showHover('newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
<!-- New File button -->
<button
@click="showHover('newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
>
<i class="material-icons">note_add</i>
<span>{{ $t("sidebar.newFile") }}</span>
</button>
<!-- Upload button -->
<button id="upload-button" @click="uploadFunc" class="action">
<i class="material-icons">file_upload</i>
<span>Upload file</span>
</button>
</div>
<div v-if="isLoggedIn" class="sources card"> <div v-if="isLoggedIn" class="sources card">
<span>Sources</span> <span>Sources</span>
<div class="inner-card"> <div class="inner-card">
@ -138,7 +109,6 @@ import { files } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "@/components/ProgressBar.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes"; import { getHumanReadableFilesize } from "@/utils/filesizes";
import { state, getters, mutations } from "@/store"; // Import your custom store import { state, getters, mutations } from "@/store"; // Import your custom store
import { showError } from "@/notify";
export default { export default {
name: "SidebarGeneral", name: "SidebarGeneral",
@ -192,13 +162,13 @@ export default {
this.hoverText = "Quick Toggles"; // Reset to default hover text this.hoverText = "Quick Toggles"; // Reset to default hover text
}, },
toggleClick() { toggleClick() {
mutations.updateUser({ singleClick: !state.user.singleClick }); mutations.updateCurrentUser({ singleClick: !state.user.singleClick });
}, },
toggleDarkMode() { toggleDarkMode() {
mutations.toggleDarkMode(); mutations.toggleDarkMode();
}, },
toggleSticky() { toggleSticky() {
mutations.updateUser({ stickySidebar: !state.user.stickySidebar }); mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
}, },
async updateUsage() { async updateUsage() {
if (!getters.isLoggedIn()) { if (!getters.isLoggedIn()) {
@ -209,21 +179,16 @@ export default {
if (this.disableUsedPercentage) { if (this.disableUsedPercentage) {
return usageStats; return usageStats;
} }
try { let usage = await files.usage(path);
let usage = await files.usage(path); usageStats = {
usageStats = { used: getHumanReadableFilesize(usage.used / 1024),
used: getHumanReadableFilesize(usage.used / 1024), total: getHumanReadableFilesize(usage.total / 1024),
total: getHumanReadableFilesize(usage.total / 1024), usedPercentage: Math.round((usage.used / usage.total) * 100),
usedPercentage: Math.round((usage.used / usage.total) * 100), };
};
} catch (error) {
showError("Error fetching usage", error);
}
mutations.setUsage(usageStats); mutations.setUsage(usageStats);
}, },
showHover(value) {
return mutations.showHover(value);
},
navigateTo(path) { navigateTo(path) {
const hashIndex = path.indexOf("#"); const hashIndex = path.indexOf("#");
if (hashIndex !== -1) { if (hashIndex !== -1) {
@ -241,9 +206,7 @@ export default {
help() { help() {
mutations.showHover("help"); mutations.showHover("help");
}, },
uploadFunc() {
mutations.showHover("upload");
},
// Logout the user // Logout the user
logout: auth.logout, logout: auth.logout,
}, },

View File

@ -7,28 +7,38 @@
@click="setView(setting.id + '-main')" @click="setView(setting.id + '-main')"
:class="{ 'active-settings': active(setting.id + '-main') }" :class="{ 'active-settings': active(setting.id + '-main') }"
> >
<div class="card-wrapper">{{ setting.label }}</div> <div v-if="shouldShow(setting)" class="card-wrapper">{{ setting.label }}</div>
</div> </div>
</template> </template>
<script> <script>
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
import { settings } from "@/utils/constants"; import { settings } from "@/utils/constants";
import { router } from "@/router";
export default { export default {
name: "SidebarSettings", name: "SidebarSettings",
data() { data() {
return { return {
settings // Initialize the settings array in data settings, // Initialize the settings array in data
}; };
}, },
computed: { computed: {
currentHash: () => getters.currentHash(), currentHash: () => getters.currentHash(),
}, },
methods: { methods: {
shouldShow(setting) {
const perm = setting?.perm || {};
// Check if all keys in setting.perm exist in state.user.perm and have truthy values
return Object.keys(perm).every((key) => state.user.perm[key]);
},
active: (view) => state.activeSettingsView === view, active: (view) => state.activeSettingsView === view,
setView(view) { setView(view) {
mutations.setActiveSettingsView(view); if (state.route.path != "/settings") {
router.push({ path: "/settings", hash: "#" + view }, () => {});
} else {
mutations.setActiveSettingsView(view);
}
}, },
}, },
}; };

View File

@ -31,7 +31,7 @@
<script> <script>
import { version, commitSHA } from "@/utils/constants"; import { version, commitSHA } from "@/utils/constants";
import { state, getters, mutations } from "@/store"; // Import your custom store import { getters, mutations } from "@/store"; // Import your custom store
import SidebarGeneral from "./General.vue"; import SidebarGeneral from "./General.vue";
import SidebarSettings from "./Settings.vue"; import SidebarSettings from "./Settings.vue";
@ -43,6 +43,7 @@ export default {
}, },
computed: { computed: {
version: () => version, version: () => version,
commitSHA: () => commitSHA,
isDarkMode: () => getters.isDarkMode(), isDarkMode: () => getters.isDarkMode(),
isLoggedIn: () => getters.isLoggedIn(), isLoggedIn: () => getters.isLoggedIn(),
isSettings: () => getters.isSettings(), isSettings: () => getters.isSettings(),

View File

@ -92,7 +92,9 @@ main > div {
} }
.breadcrumbs { .breadcrumbs {
height: 3em; overflow-x: auto;
height: auto;
min-height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex; display: flex;
align-items: center; align-items: center;
@ -162,23 +164,25 @@ button:disabled {
} }
#popup-notification { #popup-notification {
color: white; border-radius: 1em;
color: #fff;
position: fixed; position: fixed;
max-width: 90vw; max-width: 90vw;
height: 4em; height: 4em;
bottom: 0; bottom: 0;
right: -20em; /* Start off-screen */ right: -20em;
display: flex; display: flex;
padding: 1em; padding: 0.5em;
align-items: center; align-items: center;
transition: right 1s ease; /* Animate the 'right' property */ transition: right 1s ease;
z-index: 5; z-index: 5;
margin: 1em;
height: auto;
} }
#popup-notification-content { #popup-notification-content {
color: white; color: white;
padding: 0; padding: 1em;
padding-left: .5em;
} }
#popup-notification.success { #popup-notification.success {

View File

@ -103,6 +103,10 @@
border-color: var(--divider) !important; border-color: var(--divider) !important;
} }
.dark-mode #listingView.gallery .item .text {
text-shadow: 0 0 2px black;
}
/* Listing item modified text */ /* Listing item modified text */
.dark-mode #listingView .item .modified { .dark-mode #listingView .item .modified {
color: var(--textSecondary); color: var(--textSecondary);

View File

@ -36,6 +36,7 @@ body.rtl #listingView {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
overflow:hidden;
} }
#listingView .item div:last-of-type { #listingView .item div:last-of-type {
@ -140,7 +141,7 @@ body.rtl #listingView {
display:flex; display:flex;
min-width: 12em; min-width: 12em;
min-height: 12em; min-height: 12em;
text-shadow: 0 0 2px black; text-shadow: 0 0 2px white;
} }
#listingView.gallery .item div:last-of-type { #listingView.gallery .item div:last-of-type {
@ -407,25 +408,3 @@ body.rtl #listingView {
#listingView.list .header .active { #listingView.list .header .active {
font-weight: bold; font-weight: bold;
} }
#listingView #multiple-selection {
position: fixed;
bottom: -4em;
left: 0;
z-index: 99999;
width: 100%;
background-color: var(--blue);
height: 4em;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
transition: .2s ease bottom;
}
#listingView #multiple-selection.active {
bottom: 0;
}
#listingView #multiple-selection p,
#listingView #multiple-selection i {
color: var(--item-selected);
}

View File

@ -43,6 +43,8 @@
padding: .5em; padding: .5em;
text-align: center; text-align: center;
animation: .2s opac forwards; animation: .2s opac forwards;
margin-bottom: 0.5em;
border-radius: 1em;
} }
@keyframes opac { @keyframes opac {

View File

@ -174,6 +174,7 @@
"video": "Video" "video": "Video"
}, },
"settings": { "settings": {
"UserManagement": "User Management",
"admin": "Admin", "admin": "Admin",
"administrator": "Administrator", "administrator": "Administrator",
"allowCommands": "Execute commands", "allowCommands": "Execute commands",

View File

@ -175,7 +175,7 @@
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)", "avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
"branding": "Marka", "branding": "Marka",
"brandingDirectoryPath": "Marka dizin yolu", "brandingDirectoryPath": "Marka dizin yolu",
"brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak Filebrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.", "brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak FileBrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.",
"changePassword": "Şifre Değiştir", "changePassword": "Şifre Değiştir",
"commandRunner": "Komut satırı", "commandRunner": "Komut satırı",
"commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.", "commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.",

View File

@ -1,7 +1,5 @@
import { showSuccess, showError, closePopUp } from "./message.js"; import * as notify from "./message.js";
export { export {
showSuccess, notify,
showError,
closePopUp,
}; };

View File

@ -1,20 +1,35 @@
import { mutations, state } from "@/store";
export function showPopup(type, message) { export function showPopup(type, message) {
const [popup, popupContent] = getElements(); const [popup, popupContent] = getElements();
if (popup == undefined) {
return
}
popup.classList.remove('success', 'error'); // Clear previous types popup.classList.remove('success', 'error'); // Clear previous types
popup.classList.add(type); popup.classList.add(type);
popupContent.textContent = message; popupContent.textContent = message;
// Start animation: bring the popup into view
popup.style.right = '1em'; popup.style.right = '1em';
// don't hide for actions
if (type == "action") {
popup.classList.add("success");
return
}
// Start animation: bring the popup into view
// Automatically hide after 10 seconds // Automatically hide after 10 seconds
setTimeout(() => { setTimeout(() => {
closePopUp() closePopUp()
}, 10000); }, 10000)
} }
export function closePopUp() { export function closePopUp() {
const [popup, popupContent] = getElements(); const [popup, popupContent] = getElements();
if (popupContent == undefined) {
return
}
if (popupContent.textContent == "Multiple Selection Enabled" && state.multiple) {
mutations.setMultiple(false)
}
popup.style.right = '-50em'; // Slide out popup.style.right = '-50em'; // Slide out
popupContent.textContent = "no content"; popupContent.textContent = "no content";
} }
@ -22,14 +37,12 @@ export function closePopUp() {
function getElements() { function getElements() {
const popup = document.getElementById('popup-notification'); const popup = document.getElementById('popup-notification');
if (!popup) { if (!popup) {
console.error('Popup notification element not found');
return [null, null]; return [null, null];
} }
const popupContent = popup.querySelector('#popup-notification-content'); const popupContent = popup.querySelector('#popup-notification-content');
if (!popupContent) { if (!popupContent) {
console.error('Popup notification content element not found'); return [null, null];
return [null, null];
} }
return [popup, popupContent]; return [popup, popupContent];
@ -42,4 +55,8 @@ export function showSuccess(message) {
export function showError(message) { export function showError(message) {
showPopup('error', message); showPopup('error', message);
console.error(message) console.error(message)
}
export function showMultipleSelection() {
showPopup("action","Multiple Selection Enabled");
} }

View File

@ -70,6 +70,14 @@ const routes = [
name: "Settings", name: "Settings",
component: Settings, component: Settings,
}, },
{
path: "users/:id",
name: "User",
component: Settings,
meta: {
requiresAdmin: true,
},
},
], ],
}, },
{ {

View File

@ -1,7 +1,7 @@
import { state } from "./state.js"; import { state } from "./state.js";
export const getters = { export const getters = {
isResizableView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" , isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
currentHash: () => state.route.hash.replace("#", ""), currentHash: () => state.route.hash.replace("#", ""),
isMobile: () => state.isMobile, isMobile: () => state.isMobile,
isLoading: () => Object.keys(state.loading).length > 0, isLoading: () => Object.keys(state.loading).length > 0,
@ -19,6 +19,7 @@ export const getters = {
isFiles: () => state.route.name === "Files", isFiles: () => state.route.name === "Files",
isListing: () => getters.isFiles() && state.req.isDir, isListing: () => getters.isFiles() && state.req.isDir,
selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0, selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
getFirstSelected: () => state.req.items[state.selected[0]],
isSingleFileSelected: () => getters.selectedCount() === 1 && !state.req.items[state.selected[0]]?.isDir, isSingleFileSelected: () => getters.selectedCount() === 1 && !state.req.items[state.selected[0]]?.isDir,
selectedDownloadUrl() { selectedDownloadUrl() {
let selectedItem = state.selected[0] let selectedItem = state.selected[0]
@ -77,7 +78,7 @@ export const getters = {
return { dirs, files }; return { dirs, files };
}, },
isSidebarVisible: () => { isSidebarVisible: () => {
let visible = state.showSidebar || getters.isStickySidebar() let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
if (getters.currentView() == "settings") { if (getters.currentView() == "settings") {
visible = !getters.isMobile(); visible = !getters.isMobile();
} }

View File

@ -3,6 +3,7 @@ import { state } from "./state.js";
import router from "@/router"; import router from "@/router";
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
import { users } from "@/api"; import { users } from "@/api";
import { notify } from "@/notify";
export const mutations = { export const mutations = {
setGallerySize: (value) => { setGallerySize: (value) => {
@ -31,13 +32,13 @@ export const mutations = {
emitStateChanged(); emitStateChanged();
}, },
toggleDarkMode() { toggleDarkMode() {
mutations.updateUser({ "darkMode": !state.user.darkMode }); mutations.updateCurrentUser({ "darkMode": !state.user.darkMode });
emitStateChanged(); emitStateChanged();
}, },
toggleSidebar() { toggleSidebar() {
if (state.user.stickySidebar) { if (state.user.stickySidebar) {
localStorage.setItem("stickySidebar", "false"); localStorage.setItem("stickySidebar", "false");
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed mutations.updateCurrentUser({ "stickySidebar": false }); // turn off sticky when closed
state.showSidebar = false; state.showSidebar = false;
} else { } else {
state.showSidebar = !state.showSidebar; state.showSidebar = !state.showSidebar;
@ -94,27 +95,23 @@ export const mutations = {
state.loading = { ...state.loading, [loadType]: true }; state.loading = { ...state.loading, [loadType]: true };
} }
emitStateChanged(); emitStateChanged();
}, },
setReload: (value) => { setReload: (value) => {
state.reload = value; state.reload = value;
emitStateChanged(); emitStateChanged();
}, },
setUser: (value) => { setCurrentUser: (value) => {
if (value === null) { state.user = value;
state.user = null; // If value is null or undefined, emit state change and exit early
if (!value) {
emitStateChanged(); emitStateChanged();
return; return;
} }
// Ensure locale exists and is valid
let locale = value.locale; if (!value.locale) {
if (locale === "") { value.locale = i18n.detectLocale(); // Default to detected locale if missing
value.locale = i18n.detectLocale();
}
let previousUser = state.user
state.user = value;
if (state.user != previousUser && state.user.username != "publicUser") {
users.update(state.user);
} }
// Emit state change after setting the user and locale
emitStateChanged(); emitStateChanged();
}, },
setJWT: (value) => { setJWT: (value) => {
@ -127,6 +124,11 @@ export const mutations = {
}, },
setMultiple: (value) => { setMultiple: (value) => {
state.multiple = value; state.multiple = value;
if (value == true) {
notify.showMultipleSelection()
} else {
notify.closePopUp()
}
emitStateChanged(); emitStateChanged();
}, },
addSelected: (value) => { addSelected: (value) => {
@ -144,22 +146,38 @@ export const mutations = {
mutations.setMultiple(false); mutations.setMultiple(false);
emitStateChanged(); emitStateChanged();
}, },
updateUser: (value) => { updateCurrentUser: (value) => {
if (typeof value !== "object") return; // Ensure the input is a valid object
if (state.user === null) { if (typeof value !== "object" || value === null) return;
// Initialize state.user if it's null
if (!state.user) {
state.user = {}; state.user = {};
} }
let previousUser = state.user;
// Store previous state for comparison
const previousUser = { ...state.user };
// Merge the new values into the current user state
state.user = { ...state.user, ...value }; state.user = { ...state.user, ...value };
// Handle locale change
if (state.user.locale !== previousUser.locale) { if (state.user.locale !== previousUser.locale) {
state.user.locale = i18n.detectLocale(); state.user.locale = i18n.detectLocale();
i18n.setLocale(state.user.locale); i18n.setLocale(state.user.locale);
i18n.default.locale = state.user.locale; i18n.default.locale = state.user.locale;
} }
localStorage.setItem("stickySidebar", state.user.stickySidebar);
if (state.user != previousUser) { // Update localStorage if stickySidebar exists
users.update(state.user); if ('stickySidebar' in state.user) {
localStorage.setItem("stickySidebar", state.user.stickySidebar);
} }
// Update users if there's any change in state.user
if (JSON.stringify(state.user) !== JSON.stringify(previousUser)) {
users.update(state.user,Object.keys(value));
}
// Emit state change event
emitStateChanged(); emitStateChanged();
}, },
updateRequest: (value) => { updateRequest: (value) => {

View File

@ -13,7 +13,7 @@ export function parseToken(token) {
localStorage.setItem("jwt", token); localStorage.setItem("jwt", token);
mutations.setJWT(token); mutations.setJWT(token);
mutations.setSession(generateRandomCode(8)); mutations.setSession(generateRandomCode(8));
mutations.setUser(data.user); mutations.setCurrentUser(data.user);
} }
export async function validateLogin() { export async function validateLogin() {
@ -89,7 +89,7 @@ export async function signupLogin(username, password) {
export function logout() { export function logout() {
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"; document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setJWT(""); mutations.setJWT("");
mutations.setUser(null); mutations.setCurrentUser(null);
localStorage.setItem("jwt", null); localStorage.setItem("jwt", null);
router.push({ path: "/login" }); router.push({ path: "/login" });
} }

View File

@ -1,4 +1,4 @@
const name = window.FileBrowser.Name || "File Browser"; const name = window.FileBrowser.Name || "FileBrowser Quantum";
const disableExternal = window.FileBrowser.DisableExternal; const disableExternal = window.FileBrowser.DisableExternal;
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage; const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
const baseURL = window.FileBrowser.BaseURL; const baseURL = window.FileBrowser.BaseURL;
@ -20,9 +20,13 @@ const origin = window.location.origin;
const settings = [ const settings = [
{ id: 'profile', label: 'Profile Management', component: 'ProfileSettings' }, { id: 'profile', label: 'Profile Management', component: 'ProfileSettings' },
{ id: 'shares', label: 'Share Management', component: 'SharesSettings' }, {
{ id: 'global', label: 'Global', component: 'GlobalSettings' }, id: 'shares', label: 'Share Management', component: 'SharesSettings', perm: {
{ id: 'user-defaults', label: 'User Defaults', component: 'UserDefaultSettings' }, share: true
}
},
{ id: 'global', label: 'Global', component: 'GlobalSettings', perm: { admin: true } },
{ id: 'users', label: 'User Management', component: 'UserManagement', perm: { admin: true } },
] ]
export { export {

View File

@ -0,0 +1,30 @@
import { state, mutations, getters } from "@/store"
import { files as api } from "@/api";
import { notify } from "@/notify"
export default function download() {
if (getters.isSingleFileSelected()) {
api.download(null, getters.selectedDownloadUrl());
return;
}
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
try {
api.download(format, ...files);
notify.showSuccess("download started");
} catch (e) {
notify.showError("error downloading", e);
}
},
});
}

View File

@ -101,7 +101,7 @@ export function scanFiles(dt) {
}); });
} }
export function handleFiles(files, base, overwrite = false) { export async function handleFiles(files, base, overwrite = false) {
for (const file of files) { for (const file of files) {
const id = state.upload.id; const id = state.upload.id;
let path = base; let path = base;
@ -123,8 +123,7 @@ export function handleFiles(files, base, overwrite = false) {
overwrite, overwrite,
}; };
// Upload the file using your API await api.post(item.path, item.file, item.overwrite, (event) => {
api.post(item.path, item.file, item.overwrite, (event) => {
console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`); console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
}) })
.then(response => { .then(response => {

View File

@ -8,7 +8,8 @@
</template> </template>
<script> <script>
import { state } from "@/store";
import { router } from "@/router";
const errors = { const errors = {
0: { 0: {
icon: "cloud_off", icon: "cloud_off",
@ -30,13 +31,26 @@ const errors = {
export default { export default {
name: "errors", name: "errors",
components: { components: {},
},
props: ["errorCode", "showHeader"], props: ["errorCode", "showHeader"],
computed: { computed: {
info() { info() {
return errors[this.errorCode] ? errors[this.errorCode] : errors[500]; return errors[this.errorCode] ? errors[this.errorCode] : errors[500];
}, },
}, },
mounted() {
window.addEventListener("keydown", this.keyEvent);
},
methods: {
keyEvent(event) {
const { key } = event;
if (key == "Backspace") {
// go back
let currentPath = state.route.path.replace(/\/+$/, "");
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
router.push({ path: newPath });
}
},
},
}; };
</script> </script>

View File

@ -24,7 +24,7 @@
<i v-on:click="closePopUp" class="material-icons">close</i> <i v-on:click="closePopUp" class="material-icons">close</i>
<div id="popup-notification-content">no info</div> <div id="popup-notification-content">no info</div>
</div> </div>
<fileSelection> </fileSelection> <ContextMenu></ContextMenu>
</template> </template>
<script> <script>
import editorBar from "./bars/EditorBar.vue"; import editorBar from "./bars/EditorBar.vue";
@ -33,16 +33,16 @@ import listingBar from "./bars/ListingBar.vue";
import Prompts from "@/components/prompts/Prompts.vue"; import Prompts from "@/components/prompts/Prompts.vue";
import Sidebar from "@/components/sidebar/Sidebar.vue"; import Sidebar from "@/components/sidebar/Sidebar.vue";
import Search from "@/components/Search.vue"; import Search from "@/components/Search.vue";
import fileSelection from "@/components/FileSelection.vue"; import ContextMenu from "@/components/ContextMenu.vue";
import { closePopUp } from "@/notify"; import { notify } from "@/notify";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
export default { export default {
name: "layout", name: "layout",
components: { components: {
fileSelection, ContextMenu,
Search, Search,
defaultBar, defaultBar,
editorBar, editorBar,
@ -72,7 +72,7 @@ export default {
return getters.isSidebarVisible() && getters.isStickySidebar(); return getters.isSidebarVisible() && getters.isStickySidebar();
}, },
closePopUp() { closePopUp() {
return closePopUp; return notify.closePopUp;
}, },
progress() { progress() {
return getters.progress(); // Access getter directly from the store return getters.progress(); // Access getter directly from the store
@ -130,6 +130,9 @@ export default {
</script> </script>
<style> <style>
#layout-container {
padding-bottom: 30% !important;
}
main { main {
-ms-overflow-style: none; /* Internet Explorer 10+ */ -ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="login" :class="{ recaptcha: recaptcha, 'dark-mode': isDarkMode }"> <div id="login" :class="{ recaptcha: recaptcha, 'dark-mode': isDarkMode }">
<form @submit="submit"> <form @submit="submit">
<img :src="logoURL" alt="File Browser" /> <img :src="logoURL" alt="FileBrowser Quantum" />
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
<div v-if="error !== ''" class="wrong">{{ error }}</div> <div v-if="error !== ''" class="wrong">{{ error }}</div>
@ -42,6 +42,7 @@
</template> </template>
<script> <script>
import router from "@/router";
import { state } from "@/store"; import { state } from "@/store";
import { signupLogin, login } from "@/utils/auth"; import { signupLogin, login } from "@/utils/auth";
import { import {
@ -113,7 +114,7 @@ export default {
await signupLogin(this.username, this.password); await signupLogin(this.username, this.password);
} }
await login(this.username, this.password, captcha); await login(this.username, this.password, captcha);
this.$router.push({ path: redirect }); router.push({ path: redirect });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e.message == 409) { if (e.message == 409) {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dashboard"> <div class="dashboard" style="padding-bottom: 30vh">
<div class="settings-views"> <div v-if="isRootSettings" class="settings-views">
<div <div
v-for="setting in settings" v-for="setting in settings"
:key="setting.id + '-main'" :key="setting.id + '-main'"
@ -12,7 +12,12 @@
@click="!active(setting.id + '-main') && setView(setting.id + '-main')" @click="!active(setting.id + '-main') && setView(setting.id + '-main')"
> >
<!-- Dynamically render the component based on the setting --> <!-- Dynamically render the component based on the setting -->
<component :is="setting.component"></component> <component v-if="shouldShow(setting)" :is="setting.component"></component>
</div>
</div>
<div v-else class="settings-views">
<div class="active">
<UserSettings />
</div> </div>
</div> </div>
@ -33,17 +38,17 @@
import { state, getters, mutations } from "@/store"; import { state, getters, mutations } from "@/store";
import { settings } from "@/utils/constants"; import { settings } from "@/utils/constants";
import GlobalSettings from "@/views/settings/Global.vue"; import GlobalSettings from "@/views/settings/Global.vue";
import UserDefaultSettings from "@/views/settings/UserDefaults.vue";
import UserColumnSettings from "@/views/settings/UserColumn.vue";
import ProfileSettings from "@/views/settings/Profile.vue"; import ProfileSettings from "@/views/settings/Profile.vue";
import SharesSettings from "@/views/settings/Shares.vue"; import SharesSettings from "@/views/settings/Shares.vue";
import UserManagement from "@/views/settings/Users.vue";
import UserSettings from "@/views/settings/User.vue";
export default { export default {
name: "settings", name: "settings",
components: { components: {
UserManagement,
UserSettings,
GlobalSettings, GlobalSettings,
UserDefaultSettings,
UserColumnSettings,
ProfileSettings, ProfileSettings,
SharesSettings, SharesSettings,
}, },
@ -53,6 +58,12 @@ export default {
}; };
}, },
computed: { computed: {
isRootSettings() {
return state.route.path == "/settings";
},
newUserPage() {
return state.route.path == "/settings/users/new";
},
loading() { loading() {
return getters.isLoading(); return getters.isLoading();
}, },
@ -67,6 +78,14 @@ export default {
mutations.setActiveSettingsView(getters.currentHash()); mutations.setActiveSettingsView(getters.currentHash());
}, },
methods: { methods: {
shouldShow(setting) {
if (state.isMobile) {
const perm = setting?.perm || {};
// Check if all keys in setting.perm exist in state.user.perm and have truthy values
return Object.keys(perm).every((key) => state.user.perm[key]);
}
return this.active(setting.id + "-main");
},
active(id) { active(id) {
return state.activeSettingsView === id; return state.activeSettingsView === id;
}, },
@ -88,6 +107,7 @@ export default {
.settings-views { .settings-views {
max-width: 1000px; max-width: 1000px;
padding-bottom: 35vh; padding-bottom: 35vh;
width: 100%;
} }
.settings-views > .active > .card { .settings-views > .active > .card {
border-style: solid; border-style: solid;

View File

@ -119,20 +119,6 @@
readOnly readOnly
> >
</item> </item>
<div :class="{ active: multiple }" id="multiple-selection">
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
<div
@click="setMultipleFalse"
tabindex="0"
role="button"
:title="$t('files.clear')"
:aria-label="$t('files.clear')"
class="action"
>
<i class="material-icons">clear</i>
</div>
</div>
</div> </div>
</div> </div>
<div <div
@ -149,7 +135,7 @@
</div> </div>
</template> </template>
<script> <script>
import { showSuccess } from "@/notify"; import { notify } from "@/notify";
import { getHumanReadableFilesize } from "@/utils/filesizes"; import { getHumanReadableFilesize } from "@/utils/filesizes";
import { pub as api } from "@/api"; import { pub as api } from "@/api";
import { fromNow } from "@/utils/moment"; import { fromNow } from "@/utils/moment";
@ -184,15 +170,14 @@ export default {
}, },
}, },
created() { created() {
const hash = state.route.params.path.at(-1); this.hash = state.route.params.path.at(0);
this.hash = hash;
this.fetchData(); this.fetchData();
}, },
mounted() { mounted() {
window.addEventListener("keydown", this.keyEvent); window.addEventListener("keydown", this.keyEvent);
this.clip = new Clipboard(".copy-clipboard"); this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => { this.clip.on("success", () => {
showSuccess(this.$t("success.linkCopied")); notify.showSuccess(this.$t("success.linkCopied"));
}); });
}, },
beforeUnmount() { beforeUnmount() {
@ -226,10 +211,19 @@ export default {
return "insert_drive_file"; return "insert_drive_file";
}, },
link() { link() {
return api.getDownloadURL(state.req); return api.getDownloadURL({
hash: this.hash,
path: window.location.pathname,
});
}, },
inlineLink() { inlineLink() {
return api.getDownloadURL(state.req, true); return api.getDownloadURL(
{
hash: this.hash,
path: window.location.pathname,
},
true
);
}, },
humanSize() { humanSize() {
if (state.req.isDir) { if (state.req.isDir) {
@ -262,7 +256,7 @@ export default {
// Reset view information. // Reset view information.
if (!getters.isLoggedIn()) { if (!getters.isLoggedIn()) {
let userData = await api.getPublicUser(); let userData = await api.getPublicUser();
mutations.setUser(userData); mutations.setCurrentUser(userData);
} }
mutations.setReload(false); mutations.setReload(false);
mutations.resetSelected(); mutations.resetSelected();
@ -324,3 +318,8 @@ export default {
}, },
}; };
</script> </script>
<style>
.share {
padding-bottom: 35vh;
}
</style>

View File

@ -15,11 +15,11 @@
</style> </style>
<script> <script>
import url from "@/utils/url" import url from "@/utils/url";
import router from "@/router"; import router from "@/router";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { files as api } from "@/api"; import { files as api } from "@/api";
import Action from "@/components/header/Action.vue"; import Action from "@/components/Action.vue";
import css from "@/utils/css"; import css from "@/utils/css";
export default { export default {
@ -214,7 +214,7 @@ export default {
close() { close() {
if (getters.isSettings()) { if (getters.isSettings()) {
// Use this.isSettings to access the computed property // Use this.isSettings to access the computed property
router.push({ path: "/files/",hash: "" }); router.push({ path: "/files/", hash: "" });
mutations.closeHovers(); mutations.closeHovers();
return; return;
} }
@ -296,7 +296,8 @@ export default {
mutations.closeHovers(); mutations.closeHovers();
const currentIndex = this.viewModes.indexOf(state.user.viewMode); const currentIndex = this.viewModes.indexOf(state.user.viewMode);
const nextIndex = (currentIndex + 1) % this.viewModes.length; const nextIndex = (currentIndex + 1) % this.viewModes.length;
mutations.updateUser({ viewMode: this.viewModes[nextIndex] }); const newView = this.viewModes[nextIndex];
mutations.updateCurrentUser({ "viewMode": newView });
}, },
preventDefault(event) { preventDefault(event) {
// Wrapper around prevent default. // Wrapper around prevent default.

View File

@ -30,9 +30,9 @@ import { state, mutations } from "@/store";
import { eventBus } from "@/store/eventBus"; import { eventBus } from "@/store/eventBus";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import url from "@/utils/url"; import url from "@/utils/url";
import { showError, showSuccess } from "@/notify"; import { notify } from "@/notify";
import Action from "@/components/header/Action.vue"; import Action from "@/components/Action.vue";
export default { export default {
name: "editorBar", name: "editorBar",
@ -108,10 +108,10 @@ export default {
try { try {
eventBus.emit("handleEditorValueRequest", "data"); eventBus.emit("handleEditorValueRequest", "data");
buttons.success(button); buttons.success(button);
showSuccess("File Saved!"); notify.showSuccess("File Saved!");
} catch (e) { } catch (e) {
buttons.done(button); buttons.done(button);
showError("Error saving file: ", e); notify.showError("Error saving file: ", e);
} }
}, },
close() { close() {

View File

@ -26,8 +26,7 @@
</style> </style>
<script> <script>
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import Action from "@/components/header/Action.vue"; import Action from "@/components/Action.vue";
import { showError } from "@/notify";
export default { export default {
name: "listingView", name: "listingView",
@ -79,11 +78,7 @@ export default {
const currentIndex = this.viewModes.indexOf(state.user.viewMode); const currentIndex = this.viewModes.indexOf(state.user.viewMode);
const nextIndex = (currentIndex + 1) % this.viewModes.length; const nextIndex = (currentIndex + 1) % this.viewModes.length;
const newView = this.viewModes[nextIndex]; const newView = this.viewModes[nextIndex];
try { mutations.updateCurrentUser({ "viewMode": newView });
mutations.updateUser({ viewMode: newView });
} catch (e) {
showError(e);
}
}, },
}, },
}; };

View File

@ -5,9 +5,9 @@
</template> </template>
<script> <script>
import { router } from "@/router";
import { eventBus } from "@/store/eventBus"; import { eventBus } from "@/store/eventBus";
import { state, mutations, getters } from "@/store"; import { state, getters } from "@/store";
import { showError } from "@/notify";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import ace from "ace-builds/src-min-noconflict/ace.js"; import ace from "ace-builds/src-min-noconflict/ace.js";
@ -62,7 +62,8 @@ export default {
}, },
mounted: function () { mounted: function () {
// this is empty content string "empty-file-x6OlSil" which is used to represent empty text file // this is empty content string "empty-file-x6OlSil" which is used to represent empty text file
const fileContent = state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || ""; const fileContent =
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
this.editor = ace.edit("editor", { this.editor = ace.edit("editor", {
value: fileContent, value: fileContent,
showPrintMargin: false, showPrintMargin: false,
@ -79,31 +80,36 @@ export default {
}, },
methods: { methods: {
handleEditorValueRequest() { handleEditorValueRequest() {
try { api.put(state.route.path, this.editor.getValue());
api.put(state.route.path, this.editor.getValue());
} catch (e) {
showError(e);
}
}, },
back() { back() {
let uri = url.removeLastDir(state.route.path) + "/"; let uri = url.removeLastDir(state.route.path) + "/";
this.$router.push({ path: uri }); this.$router.push({ path: uri });
}, },
keyEvent(event) { keyEvent(event) {
if (!event.ctrlKey && !event.metaKey) { const { key, ctrlKey, metaKey } = event;
if (getters.currentPromptName() != null) {
return; return;
} }
if (key == "Backspace") {
// go back
let currentPath = state.route.path.replace(/\/+$/, "");
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
router.push({ path: newPath });
}
if (!ctrlKey && !metaKey) {
return;
}
switch (key.toLowerCase()) {
case "s":
event.preventDefault();
this.save();
break;
if (String.fromCharCode(event.which).toLowerCase() !== "s") { default:
return; // No action for other keys
return;
} }
event.preventDefault();
this.save();
},
close() {
mutations.replaceRequest({});
let uri = url.removeLastDir(state.route.path) + "/";
this.$router.push({ path: uri });
}, },
}, },
}; };

View File

@ -1,5 +1,5 @@
<template> <template>
<div style="padding-bottom: 5em"> <div style="padding-bottom: 35vh">
<div v-if="loading"> <div v-if="loading">
<h2 class="message delayed"> <h2 class="message delayed">
<div class="spinner"> <div class="spinner">
@ -100,8 +100,7 @@
v-bind:type="item.type" v-bind:type="item.type"
v-bind:size="item.size" v-bind:size="item.size"
v-bind:path="item.path" v-bind:path="item.path"
> />
</item>
</div> </div>
<div v-if="numFiles > 0"> <div v-if="numFiles > 0">
<div class="header-items"> <div class="header-items">
@ -120,8 +119,7 @@
v-bind:type="item.type" v-bind:type="item.type"
v-bind:size="item.size" v-bind:size="item.size"
v-bind:path="item.path" v-bind:path="item.path"
> />
</item>
</div> </div>
<input <input
@ -129,7 +127,7 @@
type="file" type="file"
id="upload-input" id="upload-input"
@change="uploadInput($event)" @change="uploadInput($event)"
getMultiple multiple
/> />
<input <input
style="display: none" style="display: none"
@ -137,34 +135,21 @@
id="upload-folder-input" id="upload-folder-input"
@change="uploadInput($event)" @change="uploadInput($event)"
webkitdirectory webkitdirectory
getMultiple multiple
/> />
<div :class="{ active: getMultiple }" id="multiple-selection">
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
<div
@click="this.setMultiple(false)"
tabindex="0"
role="button"
:title="$t('files.clear')"
:aria-label="$t('files.clear')"
class="action"
>
<i class="material-icons">clear</i>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import download from "@/utils/download";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { router } from "@/router";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import css from "@/utils/css"; import css from "@/utils/css";
import throttle from "@/utils/throttle"; import throttle from "@/utils/throttle";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { showError } from "@/notify";
import Item from "@/components/files/ListingItem.vue"; import Item from "@/components/files/ListingItem.vue";
export default { export default {
@ -178,15 +163,39 @@ export default {
columnWidth: 250 + state.user.gallerySize * 50, columnWidth: 250 + state.user.gallerySize * 50,
dragCounter: 0, dragCounter: 0,
width: window.innerWidth, width: window.innerWidth,
lastSelected: {}, // Add this to track the currently focused item
}; };
}, },
watch: { watch: {
gallerySize() { gallerySize() {
this.columnWidth = 250 + state.user.gallerySize * 50; // Update columnWidth based on new gallery size\ this.columnWidth = 250 + state.user.gallerySize * 50;
this.colunmsResize(); this.colunmsResize();
}, },
}, },
computed: { computed: {
lastFolderIndex() {
const allItems = [...this.items.dirs, ...this.items.files];
for (let i = 0; i < allItems.length; i++) {
if (!allItems[i].isDir) {
return i - 1;
}
}
if (allItems.length > 0) {
return allItems.length;
}
return null; // Return null if there are no files
},
numColumns() {
if (!getters.isCardView()) {
return 1;
}
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
if (columns === 0) columns = 1;
return columns;
},
// Create a computed property that references the Vuex state // Create a computed property that references the Vuex state
gallerySize() { gallerySize() {
return state.user.gallerySize; return state.user.gallerySize;
@ -270,6 +279,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.lastSelected = state.selected;
// Check the columns size for the first time. // Check the columns size for the first time.
this.colunmsResize(); this.colunmsResize();
// Add the needed event listeners to the window and document. // Add the needed event listeners to the window and document.
@ -278,83 +288,294 @@ export default {
window.addEventListener("resize", this.windowsResize); window.addEventListener("resize", this.windowsResize);
if (!state.user.perm?.create) return; if (!state.user.perm?.create) return;
document.addEventListener("dragover", this.preventDefault); this.$el.addEventListener("dragover", this.preventDefault);
document.addEventListener("dragenter", this.dragEnter); this.$el.addEventListener("dragenter", this.dragEnter);
document.addEventListener("dragleave", this.dragLeave); this.$el.addEventListener("dragleave", this.dragLeave);
document.addEventListener("drop", this.drop); this.$el.addEventListener("drop", this.drop);
this.$el.addEventListener("contextmenu", this.openContext);
this.$el.addEventListener("click", this.clickClear);
}, },
beforeUnmount() { beforeUnmount() {
// Remove event listeners before destroying this page. // Remove event listeners before destroying this page.
window.removeEventListener("keydown", this.keyEvent); window.removeEventListener("keydown", this.keyEvent);
window.removeEventListener("scroll", this.scrollEvent); window.removeEventListener("scroll", this.scrollEvent);
window.removeEventListener("resize", this.windowsResize); window.removeEventListener("resize", this.windowsResize);
if (state.user && !state.user.perm?.create) return;
document.removeEventListener("dragover", this.preventDefault);
document.removeEventListener("dragenter", this.dragEnter);
document.removeEventListener("dragleave", this.dragLeave);
document.removeEventListener("drop", this.drop);
}, },
methods: { methods: {
base64(name) { base64(name) {
return window.btoa(unescape(encodeURIComponent(name))); return window.btoa(unescape(encodeURIComponent(name)));
}, },
keyEvent(event) { // Helper method to select the first item if nothing is selected
// Esc! selectFirstItem() {
if (event.keyCode === 27) { mutations.resetSelected();
mutations.resetSelected(); const allItems = [...this.items.dirs, ...this.items.files];
if (allItems.length > 0) {
mutations.addSelected(allItems[0].index);
} }
},
// Del! // Helper method to select an item by index
if (event.keyCode === 46) { selectItem(index) {
if (!state.user.perm.delete || state.selected.length === 0) return; mutations.resetSelected();
mutations.showHover("delete"); mutations.addSelected(index);
} },
// Helper method to handle selection based on arrow keys
navigateKeboardArrows(arrowKey) {
let selectedIndex = state.selected.length > 0 ? state.selected[0] : null;
// F2! if (selectedIndex === null) {
if (event.keyCode === 113) { // If nothing is selected, select the first item
if (!state.user.perm.rename || state.selected.length !== 1) return; this.selectFirstItem();
mutations.showHover("rename");
}
// Ctrl is pressed
if (!event.ctrlKey && !event.metaKey) {
return; return;
} }
let key = String.fromCharCode(event.which).toLowerCase(); const allItems = [...this.items.dirs, ...this.items.files]; // Combine files and directories
// Find the current index of the selected item
let currentIndex = allItems.findIndex((item) => item.index === selectedIndex);
// If no item is selected, select the first item
if (currentIndex === -1) {
// Check if there are any items to select
if (allItems.length > 0) {
currentIndex = 0;
this.selectItem(allItems[currentIndex].index);
}
return;
}
let newSelected = null;
const fileSelected = currentIndex > this.lastFolderIndex;
const nextIsDir = currentIndex - this.numColumns <= this.lastFolderIndex;
const folderSelected = currentIndex <= this.lastFolderIndex;
const nextIsFile = currentIndex + this.numColumns > this.lastFolderIndex;
const nextHopExists = currentIndex + this.numColumns < allItems.length;
const thisColumnNum =
((currentIndex - this.lastFolderIndex - 1) % this.numColumns) + 1;
const lastFolderColumn = (this.lastFolderIndex % this.numColumns) + 1;
const thisColumnNum2 = (currentIndex + 1) % this.numColumns;
let firstRowColumnPos = this.lastFolderIndex + thisColumnNum2;
let newPos = currentIndex - lastFolderColumn;
switch (arrowKey) {
case "ArrowUp":
if (currentIndex - this.numColumns < 0) {
// do nothing
break;
}
if (!getters.isCardView) {
newSelected = allItems[currentIndex - 1].index;
break;
}
// do normal move
if (!(fileSelected && nextIsDir)) {
newSelected = allItems[currentIndex - this.numColumns].index;
break;
}
// complex logic to move from files to folders
if (lastFolderColumn < thisColumnNum) {
newPos -= this.numColumns;
}
newSelected = allItems[newPos].index;
switch (key) {
case "f":
event.preventDefault();
mutations.showHover("search");
break; break;
case "ArrowDown":
if (currentIndex >= allItems.length) {
// do nothing - last item
break;
}
if (!getters.isCardView) {
newSelected = allItems[currentIndex + 1].index;
break;
}
if (!nextHopExists) {
// do nothing - next item is out of bounds
break;
}
if (!(folderSelected && nextIsFile)) {
newSelected = allItems[currentIndex + this.numColumns].index;
break;
}
// complex logic for moving from folders to files
if (firstRowColumnPos <= this.lastFolderIndex) {
firstRowColumnPos += this.numColumns;
}
newSelected = allItems[firstRowColumnPos].index;
break;
case "ArrowLeft":
if (currentIndex > 0) {
newSelected = allItems[currentIndex - 1].index;
}
break;
case "ArrowRight":
if (currentIndex < allItems.length - 1) {
newSelected = allItems[currentIndex + 1].index;
}
break;
}
if (newSelected != null) {
this.selectItem(newSelected);
setTimeout(() => {
// Find the element with class "item" and aria-selected="true"
const element = document.querySelector('.item[aria-selected="true"]');
// Scroll the element into view if it exists
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest",
});
}
}, 50);
}
},
keyEvent(event) {
const { key, ctrlKey, metaKey, which } = event;
// Check if the key is alphanumeric
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
const noModifierKeys = !ctrlKey && !metaKey;
if (isAlphanumeric && noModifierKeys) {
this.alphanumericKeyPress(key); // Call the alphanumeric key press function
return;
}
// Handle the space bar key
if (key === " ") {
event.preventDefault();
if (getters.currentPromptName() == "search") {
mutations.closeHovers();
} else {
mutations.showHover("search");
}
}
if (getters.currentPromptName() != null) {
return;
}
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
// Handle key events using a switch statement
switch (key) {
case "Enter":
if (this.selectedCount === 1) {
router.push({ path: getters.getFirstSelected().url });
}
break;
case "Backspace":
// go back
router.push({ path: newPath });
break;
case "Escape":
mutations.resetSelected();
break;
case "Delete":
if (!state.user.perm.delete || state.selected.length === 0) return;
mutations.showHover("delete");
break;
case "F2":
if (!state.user.perm.rename || state.selected.length !== 1) return;
mutations.showHover("rename");
break;
case "ArrowUp":
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
event.preventDefault();
this.navigateKeboardArrows(key);
break;
default:
// Handle keys with ctrl or meta keys
if (!ctrlKey && !metaKey) return;
break;
}
const charKey = String.fromCharCode(which).toLowerCase();
switch (charKey) {
case "c": case "c":
case "x": case "x":
this.copyCut(event, key); this.copyCut(event, charKey);
break; break;
case "v": case "v":
this.paste(event); this.paste(event);
break; break;
case "a": case "a":
event.preventDefault(); event.preventDefault();
for (let file of this.items.files) { this.selectAll();
if (state.selected.indexOf(file.index) === -1) {
mutations.addSelected(file.index);
}
}
for (let dir of this.items.dirs) {
if (state.selected.indexOf(dir.index) === -1) {
mutations.addSelected(dir.index);
}
}
break; break;
case "s": case "s":
event.preventDefault(); event.preventDefault();
document.getElementById("download-button").click(); download();
break; break;
} }
}, },
// Helper method to select all files and directories
selectAll() {
for (let file of this.items.files) {
if (state.selected.indexOf(file.index) === -1) {
mutations.addSelected(file.index);
}
}
for (let dir of this.items.dirs) {
if (state.selected.indexOf(dir.index) === -1) {
mutations.addSelected(dir.index);
}
}
},
alphanumericKeyPress(key) {
// Convert the key to uppercase to match the case-insensitive search
const searchLetter = key.toLowerCase();
const currentSelected = getters.getFirstSelected();
let currentName = null;
let findNextWithName = false;
if (currentSelected != undefined) {
currentName = currentSelected.name.toLowerCase();
if (currentName.startsWith(searchLetter)) {
findNextWithName = true;
}
}
// Combine directories and files (assuming they are stored in this.items.dirs and this.items.files)
const allItems = [...this.items.dirs, ...this.items.files];
let foundPrevious = false;
let firstFound = null;
// Iterate over all items to find the first one where the name starts with the searchLetter
for (let i = 0; i < allItems.length; i++) {
const itemName = allItems[i].name.toLowerCase();
if (!itemName.startsWith(searchLetter)) {
continue;
}
if (firstFound == null) {
firstFound = allItems[i].index;
}
if (!findNextWithName) {
// return first you find
this.selectItem(allItems[i].index);
return;
}
if (itemName == currentName) {
foundPrevious = true;
continue;
}
if (foundPrevious) {
this.selectItem(allItems[i].index);
return;
}
}
// select the first item again
if (firstFound != null) {
this.selectItem(firstFound);
}
},
preventDefault(event) { preventDefault(event) {
// Wrapper around prevent default. // Wrapper around prevent default.
event.preventDefault(); event.preventDefault();
@ -395,23 +616,17 @@ export default {
} }
mutations.setLoading("listing", true); mutations.setLoading("listing", true);
let action = (overwrite, rename) => { let action = (overwrite, rename) => {
api api.copy(items, overwrite, rename).then(() => {
.copy(items, overwrite, rename) mutations.setLoading("listing", false);
.then(() => { });
mutations.setLoading("listing", false);
})
.catch(showError);
}; };
if (this.clipboard.key === "x") { if (this.clipboard.key === "x") {
action = (overwrite, rename) => { action = (overwrite, rename) => {
api api.move(items, overwrite, rename).then(() => {
.move(items, overwrite, rename) this.clipboard = {};
.then(() => { mutations.setLoading("listing", false);
this.clipboard = {}; });
mutations.setLoading("listing", false);
})
.catch(showError);
}; };
} }
@ -440,12 +655,8 @@ export default {
action(false, false); action(false, false);
}, },
colunmsResize() { colunmsResize() {
let columns = Math.floor(
document.querySelector("main").offsetWidth / this.columnWidth
);
let items = css(["#listingView .item", "#listingView .item"]); let items = css(["#listingView .item", "#listingView .item"]);
if (columns === 0) columns = 1; items.style.width = `calc(${100 / this.numColumns}% - 1em)`;
items.style.width = `calc(${100 / columns}% - 1em)`;
if (state.user.viewMode == "gallery") { if (state.user.viewMode == "gallery") {
items.style.height = `${this.columnWidth / 20}em`; items.style.height = `${this.columnWidth / 20}em`;
} else { } else {
@ -483,34 +694,44 @@ export default {
} }
let files = await upload.scanFiles(dt); let files = await upload.scanFiles(dt);
const folderUpload = !!files[0].webkitRelativePath;
const uploadFiles = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folderUpload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file, // File object directly
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
let items = state.req.items; let items = state.req.items;
let path = getters.getRoutePath(); let path = getters.getRoutePath();
if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") { if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
path = el.__vue__.url; path = el.__vue__.url;
try { items = (await api.fetch(path)).items;
items = (await api.fetch(path)).items;
} catch (error) {
showError(error);
}
} }
const conflict = upload.checkConflict(files, items); const conflict = upload.checkConflict(uploadFiles, items);
if (conflict) { if (conflict) {
mutations.showHover({ mutations.showHover({
name: "replace", name: "replace",
confirm: (event) => { confirm: async (event) => {
event.preventDefault(); event.preventDefault();
mutations.closeHovers(); mutations.closeHovers();
upload.handleFiles(files, path, true); await upload.handleFiles(uploadFiles, path, true);
}, },
}); });
return; } else {
await upload.handleFiles(uploadFiles, path);
} }
mutations.setReload(true);
upload.handleFiles(files, path);
}, },
uploadInput(event) { uploadInput(event) {
mutations.closeHovers(); mutations.closeHovers();
@ -564,6 +785,7 @@ export default {
}, },
setMultiple(val) { setMultiple(val) {
mutations.setMultiple(val == true); mutations.setMultiple(val == true);
showMultipleSelection();
}, },
openSearch() { openSearch() {
this.currentPrompt = "search"; this.currentPrompt = "search";
@ -584,6 +806,23 @@ export default {
document.getElementById("upload-input").click(); document.getElementById("upload-input").click();
} }
}, },
openContext(event) {
event.preventDefault();
mutations.showHover({
name: "ContextMenu",
props: {
posX: event.clientX,
posY: event.clientY,
},
});
},
clickClear() {
const sameAsBefore = state.selected == this.lastSelected;
if (sameAsBefore && !state.multiple) {
mutations.resetSelected();
}
this.lastSelected = state.selected;
},
}, },
}; };
</script> </script>

View File

@ -198,19 +198,26 @@ export default {
this.$router.replace({ path: this.nextLink }); this.$router.replace({ path: this.nextLink });
}, },
key(event) { key(event) {
if (this.currentPrompt !== null) { if (getters.currentPromptName() != null) {
return; return;
} }
if (event.which === 13 || event.which === 39) { const { key } = event;
// right arrow
if (this.hasNext) this.next(); switch (key) {
} else if (event.which === 37) { case "ArrowRight":
// left arrow if (this.hasNext) {
if (this.hasPrevious) this.prev(); this.next();
} else if (event.which === 27) { }
// esc break;
this.close(); case "ArrowLeft":
if (this.hasPrevious) {
this.prev();
}
break;
case ("Escape", "Backspace"):
this.close();
break;
} }
}, },
async updatePreview() { async updatePreview() {
@ -222,13 +229,9 @@ export default {
this.name = decodeURIComponent(dirs[dirs.length - 1]); this.name = decodeURIComponent(dirs[dirs.length - 1]);
if (!this.listing) { if (!this.listing) {
try { const path = url.removeLastDir(state.route.path);
const path = url.removeLastDir(state.route.path); const res = await api.fetch(path);
const res = await api.fetch(path); this.listing = res.items;
this.listing = res.items;
} catch (e) {
showError(e);
}
} }
this.previousLink = ""; this.previousLink = "";

View File

@ -86,7 +86,7 @@
</template> </template>
<script> <script>
import { showSuccess, showError } from "@/notify"; import { notify } from "@/notify";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { settings as api } from "@/api"; import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
@ -140,9 +140,9 @@ export default {
try { try {
mutations.setSettings(this.selectedSettings); mutations.setSettings(this.selectedSettings);
await api.update(state.settings); await api.update(state.settings);
showSuccess(this.$t("settings.settingsUpdated")); notify.showSuccess(this.$t("settings.settingsUpdated"));
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
} }
}, },
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card" id="profile-main" :class="{ active: active }"> <div class="card" :class="{ active: active }">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("settings.profileSettings") }}</h2> <h2>{{ $t("settings.profileSettings") }}</h2>
</div> </div>
@ -97,7 +97,7 @@
</template> </template>
<script> <script>
import { showSuccess, showError } from "@/notify"; import { notify } from "@/notify";
import { state, mutations } from "@/store"; import { state, mutations } from "@/store";
import { users } from "@/api"; import { users } from "@/api";
import Languages from "@/components/settings/Languages.vue"; import Languages from "@/components/settings/Languages.vue";
@ -174,9 +174,9 @@ export default {
newUserSettings.id = state.user.id; newUserSettings.id = state.user.id;
newUserSettings.password = this.password; newUserSettings.password = this.password;
await users.update(newUserSettings, ["password"]); await users.update(newUserSettings, ["password"]);
showSuccess(this.$t("settings.passwordUpdated")); notify.showSuccess(this.$t("settings.passwordUpdated"));
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
} }
}, },
async updateSettings(event) { async updateSettings(event) {
@ -203,13 +203,13 @@ export default {
"dateFormat", "dateFormat",
"gallerySize", "gallerySize",
]); ]);
mutations.updateUser(data); mutations.updateCurrentUser(data);
if (shouldReload) { if (shouldReload) {
location.reload(); location.reload();
} }
showSuccess(this.$t("settings.settingsUpdated")); notify.showSuccess(this.$t("settings.settingsUpdated"));
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
} }
}, },
updateViewMode(updatedMode) { updateViewMode(updatedMode) {

View File

@ -1,6 +1,6 @@
<template> <template>
<errors v-if="error" :errorCode="error.status" /> <errors v-if="error" :errorCode="error.status" />
<div class="card" id="shares-main" :class="{ active: active }"> <div class="card" :class="{ active: active }">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("settings.shareManagement") }}</h2> <h2>{{ $t("settings.shareManagement") }}</h2>
</div> </div>
@ -55,7 +55,7 @@
</template> </template>
<script> <script>
import { showSuccess, showError } from "@/notify"; import { notify } from "@/notify";
import { share as api, users } from "@/api"; import { share as api, users } from "@/api";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { fromNow } from "@/utils/moment"; import { fromNow } from "@/utils/moment";
@ -95,7 +95,7 @@ export default {
mounted() { mounted() {
this.clip = new Clipboard(".copy-clipboard"); this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => { this.clip.on("success", () => {
showSuccess(this.$t("success.linkCopied")); notify.showSuccess(this.$t("success.linkCopied"));
}); });
}, },
beforeUnmount() { beforeUnmount() {
@ -126,9 +126,9 @@ export default {
try { try {
api.remove(link.hash); api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash); this.links = this.links.filter((item) => item.hash !== link.hash);
showSuccess(this.$t("settings.shareDeleted")); notify.showSuccess(this.$t("settings.shareDeleted"));
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
} }
}, },
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<errors v-if="error" :errorCode="error.status" /> <errors v-if="error" :errorCode="error.status" />
<form @submit="save" id="user-main" class="card"> <form @submit="save" class="card active">
<div class="card-title"> <div class="card-title">
<h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2> <h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
<h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2> <h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
@ -37,7 +37,7 @@ import { mutations, state } from "@/store";
import { users as api, settings } from "@/api"; import { users as api, settings } from "@/api";
import UserForm from "@/components/settings/UserForm.vue"; import UserForm from "@/components/settings/UserForm.vue";
import Errors from "@/views/Errors.vue"; import Errors from "@/views/Errors.vue";
import { showSuccess, showError } from "@/notify"; import { notify } from "@/notify";
export default { export default {
name: "user", name: "user",
@ -49,12 +49,13 @@ export default {
return { return {
error: null, error: null,
originalUser: null, originalUser: null,
user: { perm: { admin: false } }, user: {
scope: ".",
username: "",
perm: { admin: false },
},
showDelete: false, showDelete: false,
createUserDir: false, createUserDir: false,
loading: false, // Replaces Vuex state `loading`
currentPrompt: null, // Replaces Vuex getter `currentPrompt`
currentPromptName: null, // Replaces Vuex getter `currentPromptName`
}; };
}, },
created() { created() {
@ -65,14 +66,14 @@ export default {
return state.settings; return state.settings;
}, },
isNew() { isNew() {
return state.route.path === "/settings/users/new"; return state.route.path.startsWith("/settings/users/new");
}, },
}, },
watch: {
$route: "fetchData",
},
methods: { methods: {
async fetchData() { async fetchData() {
if (!state.route.path.startsWith("/settings")) {
return
}
mutations.setLoading("users", true); mutations.setLoading("users", true);
try { try {
if (this.isNew) { if (this.isNew) {
@ -87,11 +88,13 @@ export default {
id: 0, id: 0,
}; };
} else { } else {
const id = state.route.params.id; const id = Array.isArray(state.route.params.id)
? state.route.params.id.join("")
: state.route.params.id;
this.user = { ...(await api.get(id)) }; this.user = { ...(await api.get(id)) };
} }
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
this.error = e; this.error = e;
} finally { } finally {
mutations.setLoading("users", false); mutations.setLoading("users", false);
@ -101,27 +104,19 @@ export default {
mutations.showHover({ name: "deleteUser", props: { user: this.user } }); mutations.showHover({ name: "deleteUser", props: { user: this.user } });
}, },
async save(event) { async save(event) {
let user = this.user
event.preventDefault(); event.preventDefault();
let user = {
...this.originalUser,
...this.user,
};
try { try {
if (this.isNew) { if (this.isNew) {
const loc = await api.create(user); const loc = await api.create(user);
this.$router.push({ path: loc }); this.$router.push({ path: loc });
showSuccess(this.$t("settings.userCreated")); notify.showSuccess(this.$t("settings.userCreated"));
} else { } else {
await api.update(user); await api.update(user);
if (user.id === state.user.id) { notify.showSuccess(this.$t("settings.userUpdated"));
consoel.log("set user");
mutations.setUser(user);
}
showSuccess(this.$t("settings.userUpdated"));
} }
} catch (e) { } catch (e) {
showError(e); notify.showError(e);
} }
}, },
}, },

View File

@ -1,106 +0,0 @@
<template>
<errors v-if="error" :errorCode="error.status" />
<div v-if="isExecEnabled" class="card" id="userColumn-main">
<form @submit.prevent="save">
<div class="card-title">
<h2>{{ $t("settings.commandRunner") }}</h2>
</div>
<div class="card-content">
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
<code>FILE</code>
<code>SCOPE</code>
<a
class="link"
target="_blank"
href="https://filebrowser.org/configuration/command-runner"
>{{ $t("settings.documentation") }}</a
>
</i18n>
<div
v-for="(command, index) in settings.commands"
:key="index"
class="collapsible"
>
<input :id="command.name" type="checkbox" />
<label :for="command.name">
<p>{{ capitalize(command.name) }}</p>
<i class="material-icons">arrow_drop_down</i>
</label>
<div class="collapse">
<textarea
class="input input--block input--textarea"
v-model.trim="command.value"
></textarea>
</div>
</div>
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')" />
</div>
</form>
</div>
</template>
<script>
import { showSuccess } from "@/notify";
import { state, getters } from "@/store";
import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants";
//import UserForm from "@/components/settings/UserForm.vue";
//import Rules from "@/components/settings/Rules.vue";
import Errors from "@/views/Errors.vue";
export default {
name: "settings",
components: {
//UserForm,
//Rules,
Errors,
},
data: function () {
return {
error: null,
originalSettings: null,
};
},
computed: {
settings() {
return state.settings;
},
loading() {
return getters.isLoading();
},
user() {
return state.user;
},
isExecEnabled: () => enableExec,
},
methods: {
updateRules(updatedRules) {
this.settings.rules = updatedRules;
},
capitalize(name, where = "_") {
if (where === "caps") where = /(?=[A-Z])/;
let splitted = name.split(where);
name = "";
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
}
return name.slice(0, -1);
},
async save() {
try {
await api.update(state.settings);
showSuccess(this.$t("settings.settingsUpdated"));
} catch (e) {
showError(e);
}
},
},
};
</script>

View File

@ -1,87 +0,0 @@
<template>
<errors v-if="error" :errorCode="error.status" />
<div class="card" id="user-defaults-main">
<div class="card-title">
<h2>{{ $t("settings.userDefaults") }}</h2>
</div>
<div class="card-content">
<p class="small">{{ $t("settings.defaultUserDescription") }}</p>
<user-form
:isNew="false"
:isDefault="true"
:user="settings.defaults"
@update:user="updateUser"
/>
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')" />
</div>
</div>
</template>
<script>
import { showSuccess } from "@/notify";
import { state, getters } from "@/store";
import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants";
import UserForm from "@/components/settings/UserForm.vue";
//import Rules from "@/components/settings/Rules.vue";
import Errors from "@/views/Errors.vue";
export default {
name: "settings",
components: {
UserForm,
//Rules,
Errors,
},
data: function () {
return {
error: null,
originalSettings: null,
};
},
computed: {
settings() {
return state.settings;
},
loading() {
return getters.isLoading();
},
user() {
return state.user;
},
isExecEnabled: () => enableExec,
},
methods: {
updateRules(updatedRules) {
state.settings.rules = updatedRules;
},
updateUser(updatedUser) {
state.settings.defaults = updatedUser;
},
capitalize(name, where = "_") {
if (where === "caps") where = /(?=[A-Z])/;
let splitted = name.split(where);
name = "";
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
}
return name.slice(0, -1);
},
async save() {
try {
await api.update(state.settings);
showSuccess(this.$t("settings.settingsUpdated"));
} catch (e) {
showError(e);
}
},
},
};
</script>

View File

@ -1,6 +1,6 @@
<template> <template>
<errors v-if="error" :errorCode="error.status" /> <errors v-if="error" :errorCode="error.status" />
<div class="card" id="users-main"> <div class="card">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("settings.users") }}</h2> <h2>{{ $t("settings.users") }}</h2>
<router-link to="/settings/users/new" <router-link to="/settings/users/new"
@ -36,12 +36,12 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { getAllUsers } from "@/api/users"; import { getAllUsers } from "@/api/users";
import Errors from "@/views/Errors.vue"; import Errors from "@/views/Errors.vue";
import { showError } from "@/notify";
mutations.setLoading("users", true);
export default { export default {
name: "users", name: "users",
components: { components: {
@ -54,18 +54,10 @@ export default {
}; };
}, },
async created() { async created() {
mutations.setLoading("users", true);
// Set loading state to true // Set loading state to true
this.users = await getAllUsers();
try { mutations.setLoading("users", false);
// Fetch all users from the API
this.users = await getAllUsers();
} catch (e) {
showError(e);
// Handle errors
this.error = e;
} finally {
mutations.setLoading("users", false);
}
}, },
computed: { computed: {
settings() { settings() {

View File

@ -10,7 +10,7 @@ test("redirect to login", async ({ page }) => {
test("login", async ({ authPage, page, context }) => { test("login", async ({ authPage, page, context }) => {
await authPage.goto(); await authPage.goto();
await expect(page).toHaveTitle(/Login - File Browser$/); await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
await authPage.loginAs("fake", "fake"); await authPage.loginAs("fake", "fake");
await expect(authPage.wrongCredentials).toBeVisible(); await expect(authPage.wrongCredentials).toBeVisible();
@ -18,14 +18,14 @@ test("login", async ({ authPage, page, context }) => {
await authPage.loginAs(); await authPage.loginAs();
await expect(authPage.wrongCredentials).toBeHidden(); await expect(authPage.wrongCredentials).toBeHidden();
// await page.waitForURL("**/files/", { timeout: 5000 }); // await page.waitForURL("**/files/", { timeout: 5000 });
await expect(page).toHaveTitle(/.*Files - File Browser$/); await expect(page).toHaveTitle(/.*Files - FileBrowser Quantum$/);
let cookies = await context.cookies(); let cookies = await context.cookies();
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined(); expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
// await authPage.logout(); // await authPage.logout();
// await page.waitForURL("**/login", { timeout: 5000 }); // await page.waitForURL("**/login", { timeout: 5000 });
// await expect(page).toHaveTitle(/Login - File Browser$/); // await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
// cookies = await context.cookies(); // cookies = await context.cookies();
// expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined(); // expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
}); });

View File

@ -3,15 +3,17 @@
next 0.2.x release: next 0.2.x release:
- Theme configuration from settings - Theme configuration from settings
- Better media and file viewer support - File syncronization improvements
- right-click context menu
initial 0.3.0 release : initial 0.3.0 release :
- drop in replace backend db with pocketbas - database changes
- introduce jobs as replacement to runners.
- Add Job status to the sidebar - Add Job status to the sidebar
- index status. - index status.
- Job status from users - Job status from users
- upload status
Future releases: Future releases:
- Replace http routes for gorilla/mux with pocketbase - Replace http routes for gorilla/mux with pocketbase