Compare commits

...
4 Commits
Author SHA1 Message Date
Graham Steffaniak c7753dfab7 updated url redirecting (#442) 2025-03-20 12:07:40 -05:00
Graham Steffaniak 7b781971a7 updated fix user directory creation issue (#441) 2025-03-19 19:03:04 -05:00
Graham Steffaniak 45bbeb2776 updated 2025-03-18 17:10:33 -05:00
Graham Steffaniak f8a507f340 updated bugfixes (#432) 2025-03-18 17:06:13 -05:00
56 changed files with 739 additions and 560 deletions

No files matched your search

+15
View File
@@ -2,6 +2,21 @@
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.6.2-beta
**Notes**:
- Added playwright tests for bugfixes for permantent fix for stability.
(except onlyoffice since it requires integrations)
**BugFixes**:
- Context menu should only be available inside the folder/files container https://github.com/gtsteffaniak/filebrowser/issues/430
- drag and drop files from desktop to browser is fixed.
- replace prompt cancel button didn't work.
- key events on listing page not working (like delete key)
- fixed share viewing issue https://github.com/gtsteffaniak/filebrowser/issues/429
- disableUsedPercentage hides entire source https://github.com/gtsteffaniak/filebrowser/issues/438
- createUserDir fix for proxy users and new users https://github.com/gtsteffaniak/filebrowser/issues/440
## v0.6.1-beta
**New Feature**:
+1 -1
View File
@@ -1,6 +1,6 @@
FROM gtstef/playwright-base
WORKDIR /app
COPY [ "./_docker/src/regular/", "./" ]
COPY [ "./_docker/src/general/", "./" ]
WORKDIR /app/frontend
COPY [ "./frontend/", "./" ]
WORKDIR /app/backend/
@@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
globalSetup: "./global-setup",
timeout: 6000,
timeout: 5000,
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: false,
@@ -28,7 +28,7 @@ export default defineConfig({
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
baseURL: "http://127.0.0.1/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
+1 -1
View File
@@ -1,6 +1,6 @@
server:
port: 80
baseURL: "/"
baseURL: "/files/"
logging:
- levels: "info|error|debug"
sources:
@@ -11,8 +11,8 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
//globalSetup: "./global-setup",
timeout: 6000,
testDir: "./tests-proxy",
timeout: 5000,
testDir: "./tests-noauth",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
+4 -2
View File
@@ -5,6 +5,9 @@ server:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
config:
defaultEnabled: true
createUserDir: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
@@ -23,10 +26,9 @@ auth:
userDefaults:
darkMode: true
disableSettings: false
scope: "."
singleClick: false
permissions:
admin: false
modify: false
modify: true
share: false
api: false
@@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
//globalSetup: "./global-setup",
timeout: 6000,
timeout: 3000,
testDir: "./tests-proxy",
/* Run tests in files in parallel */
fullyParallel: false,
@@ -25,7 +25,7 @@ export default defineConfig({
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
actionTimeout: 3000,
//storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
-3
View File
@@ -9,7 +9,6 @@ import (
"strings"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
@@ -168,8 +167,6 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
}
u = a.GetUser(d)
files.MakeUserDirs(u)
err = a.Users.Save(u, false)
if err != nil {
return nil, err
+127
View File
@@ -0,0 +1,127 @@
package cmd
import (
"flag"
"fmt"
"os"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/version"
)
var (
configPath string
)
// return bool to indicate if the program should continue running
func runCLI() bool {
// Global flags
var help bool
// Override the default usage output to use generalUsage()
flag.Usage = generalUsage
flag.StringVar(&configPath, "c", "", "Path to the config file, default: config.yaml")
flag.BoolVar(&help, "h", false, "Get help about commands")
if configPath == "" {
configPath = os.Getenv("FILEBROWSER_CONFIG")
}
if configPath == "" {
configPath = "config.yaml"
}
// Parse global flags (before subcommands)
flag.Parse() // print generalUsage on error
// Show help if requested
if help {
generalUsage()
return false
}
// Create a new FlagSet for the 'set' subcommand
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
var user, scope, dbConfig string
var asAdmin bool
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
// Parse subcommand flags only if a subcommand is specified
if len(os.Args) > 1 {
switch os.Args[1] {
case "set":
err := setCmd.Parse(os.Args[2:])
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
userInfo := strings.Split(user, ",")
if len(userInfo) < 2 {
fmt.Printf("not enough info to create user: \"set -u username,password\", only provided %v\n", userInfo)
setCmd.PrintDefaults()
os.Exit(1)
}
username := userInfo[0]
password := userInfo[1]
ok := getStore(dbConfig)
if !ok {
logger.Fatal("could not load db info")
}
user, err := store.Users.Get(username)
if err != nil {
newUser := users.User{
Username: username,
NonAdminEditable: users.NonAdminEditable{
Password: password,
},
}
for _, source := range settings.Config.Server.SourceMap {
if source.Config.DefaultEnabled {
newUser.Scopes = append(newUser.Scopes, users.SourceScope{
Name: source.Name,
Scope: source.Config.DefaultUserScope,
})
}
}
// Create the user logic
if asAdmin {
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
} else {
logger.Info(fmt.Sprintf("Creating non-admin user: %s\n", username))
}
err = storage.CreateUser(newUser, asAdmin)
if err != nil {
logger.Error(fmt.Sprintf("could not create user: %v", err))
}
return false
}
user.Password = password
if asAdmin {
user.Perm.Admin = true
}
err = store.Users.Save(user, true)
if err != nil {
logger.Error(fmt.Sprintf("could not update user: %v", err))
}
fmt.Printf("successfully updated user: %s\n", username)
return false
case "version":
fmt.Printf(`FileBrowser Quantum - A modern web-based file manager
Version : %v
Commit : %v
Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
`, version.Version, version.CommitSHA, version.Version)
return false
}
}
return true
}
+12 -157
View File
@@ -2,11 +2,9 @@ package cmd
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
@@ -19,62 +17,20 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
"github.com/swaggo/swag"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/version"
)
func getStore(configFile string) (*storage.Storage, bool) {
var store *storage.Storage
func getStore(configFile string) bool {
// Use the config file (global flag)
settings.Initialize(configFile)
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
s, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
if err != nil {
logger.Fatal(fmt.Sprintf("could not load db info: %v", err))
}
// update source info for users if names/sources/paths might have changed
usersList, err2 := store.Users.Gets()
if err2 != nil {
logger.Fatal(fmt.Sprintf("could not load users: %v", err2))
}
// this function adds scopes as needed on startup
for _, user := range usersList {
updateUser := false
newScopes := []users.SourceScope{}
for _, source := range settings.Config.Server.SourceMap {
scopePath := ""
if !user.Perm.Admin {
scopePath = source.Config.DefaultUserScope
}
scope, err := settings.GetScopeFromSourcePath(user.Scopes, source.Path)
if err != nil {
if user.Perm.Admin || source.Config.DefaultEnabled {
newScopes = append(newScopes, users.SourceScope{Scope: scopePath, Name: source.Path}) // backend name is path
updateUser = true
}
} else {
newScopes = append(newScopes, users.SourceScope{Scope: scope, Name: source.Path}) // backend name is path
}
}
user.Scopes = newScopes
// maintain backwards compatibility, update user scope from scopes
if len(user.Scopes) == 0 {
user.Scopes = []users.SourceScope{
{
Scope: user.Scope,
Name: settings.Config.Server.DefaultSource.Path, // backend name is path
},
}
updateUser = true
}
if !updateUser {
continue
}
err := store.Users.Save(user, false)
if err != nil {
logger.Error(fmt.Sprintf("could not update user: %v", err))
}
}
return store, hasDB
store = s
return hasDB
}
func generalUsage() {
@@ -91,122 +47,20 @@ commands:
}
func StartFilebrowser() {
// Global flags
var configPath string
var help bool
// Override the default usage output to use generalUsage()
flag.Usage = generalUsage
flag.StringVar(&configPath, "c", "", "Path to the config file, default: config.yaml")
flag.BoolVar(&help, "h", false, "Get help about commands")
if configPath == "" {
configPath = os.Getenv("FILEBROWSER_CONFIG")
}
if configPath == "" {
configPath = "config.yaml"
}
// Parse global flags (before subcommands)
flag.Parse() // print generalUsage on error
// Show help if requested
if help {
generalUsage()
keepGoing := runCLI()
if !keepGoing {
return
}
// Create a new FlagSet for the 'set' subcommand
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
var user, scope, dbConfig string
var asAdmin bool
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
setCmd.BoolVar(&asAdmin, "a", false, "Create user as admin user, used in combination with -u")
setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used")
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
// Create context and channels for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{}) // Signals server has stopped
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// Parse subcommand flags only if a subcommand is specified
if len(os.Args) > 1 {
switch os.Args[1] {
case "set":
err := setCmd.Parse(os.Args[2:])
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
userInfo := strings.Split(user, ",")
if len(userInfo) < 2 {
fmt.Printf("not enough info to create user: \"set -u username,password\", only provided %v\n", userInfo)
setCmd.PrintDefaults()
os.Exit(1)
}
username := userInfo[0]
password := userInfo[1]
store, ok := getStore(dbConfig)
if !ok {
logger.Fatal("could not load db info")
}
user, err := store.Users.Get(username)
if err != nil {
newUser := users.User{
Username: username,
NonAdminEditable: users.NonAdminEditable{
Password: password,
},
}
for _, source := range settings.Config.Server.SourceMap {
if source.Config.DefaultEnabled {
newUser.Scopes = append(newUser.Scopes, users.SourceScope{
Name: source.Name,
Scope: source.Config.DefaultUserScope,
})
}
}
// Create the user logic
if asAdmin {
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
} else {
logger.Info(fmt.Sprintf("Creating non-admin user: %s\n", username))
}
err = storage.CreateUser(newUser, asAdmin)
if err != nil {
logger.Error(fmt.Sprintf("could not create user: %v", err))
}
return
}
user.Password = password
if asAdmin {
user.Perm.Admin = true
}
err = store.Users.Save(user, true)
if err != nil {
logger.Error(fmt.Sprintf("could not update user: %v", err))
}
fmt.Printf("successfully updated user: %s\n", username)
return
case "version":
fmt.Printf(`FileBrowser Quantum - A modern web-based file manager
Version : %v
Commit : %v
Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
`, version.Version, version.CommitSHA, version.Version)
return
}
}
store, dbExists := getStore(configPath)
done := make(chan struct{}) // Signals server has stopped
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
dbExists := getStore(configPath)
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
if !dbExists {
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
@@ -231,6 +85,7 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
for _, source := range settings.Config.Server.SourceMap {
go files.Initialize(source)
}
validateUserInfo()
// Start the rootCMD in a goroutine
go func() {
if err := rootCMD(ctx, store, &serverConfig, shutdownComplete); err != nil {
+65
View File
@@ -0,0 +1,65 @@
package cmd
import (
"fmt"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
)
func validateUserInfo() {
// update source info for users if names/sources/paths might have changed
usersList, err := store.Users.Gets()
if err != nil {
logger.Fatal(fmt.Sprintf("could not load users: %v", err))
}
for _, user := range usersList {
updateUserScopes(user)
}
}
func updateUserScopes(user *users.User) {
updateUser := false
newScopes := []users.SourceScope{}
for _, source := range settings.Config.Server.SourceMap {
scopePath, err := settings.GetScopeFromSourcePath(user.Scopes, source.Path)
if !user.Perm.Admin {
scopePath = source.Config.DefaultUserScope
}
if scopePath == "" {
scopePath = "/"
}
if source.Config.CreateUserDir && !user.Perm.Admin {
scopePath = fmt.Sprintf("%s%s", scopePath, users.CleanUsername(user.Username))
}
// if user doesn't yet have scope for source, add it for admins and default sources
if err != nil {
if user.Perm.Admin || source.Config.DefaultEnabled {
newScopes = append(newScopes, users.SourceScope{Scope: scopePath, Name: source.Path}) // backend name is path
updateUser = true
}
} else {
newScopes = append(newScopes, users.SourceScope{Scope: scopePath, Name: source.Path}) // backend name is path
}
user.Scopes = newScopes
}
// maintain backwards compatibility, update user scope from scopes
if len(user.Scopes) == 0 {
user.Scopes = []users.SourceScope{
{
Scope: settings.Config.Server.DefaultSource.Config.DefaultUserScope,
Name: settings.Config.Server.DefaultSource.Path, // backend name is path
},
}
updateUser = true
}
if !updateUser {
return
}
err := store.Users.Save(user, false)
if err != nil {
logger.Error(fmt.Sprintf("could not update user: %v", err))
}
}
+22 -39
View File
@@ -3,32 +3,16 @@ package files
import (
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
)
var (
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
dashes = regexp.MustCompile(`[\-]+`)
)
// todo fix this!!
// MakeUserDir makes the user directory according to settings.
func (idx *Index) MakeUserDir(username string, scope string) error {
if idx.Config.CreateUserDir {
username = cleanUsername(username)
if username == "" || username == "-" || username == "." {
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", username))
}
}
userScope := path.Join("/", scope)
fullPath := filepath.Join(idx.Path, userScope)
func MakeUserDir(fullPath string) error {
logger.Debug(fmt.Sprintf("creating user home dir: [%s]", fullPath))
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
return err
@@ -36,32 +20,31 @@ func (idx *Index) MakeUserDir(username string, scope string) error {
return nil
}
func MakeUserDirs(u *users.User) {
for _, scope := range u.Scopes {
source := settings.Config.Server.NameToSource[scope.Name]
func MakeUserDirs(u *users.User) error {
cleanedUserName := users.CleanUsername(u.Username)
if cleanedUserName == "" || cleanedUserName == "-" || cleanedUserName == "." {
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", u.Username))
}
for i, scope := range u.Scopes {
source := settings.Config.Server.SourceMap[scope.Name]
if source.Config.CreateUserDir {
idx := GetIndex(source.Name)
if idx == nil {
logger.Error(fmt.Sprintf("create user: failed to find source index for user home dir creation: %s", source.Name))
if !source.Config.CreateUserDir {
continue
}
err := idx.MakeUserDir(u.Username, scope.Scope)
if err != nil {
logger.Error(fmt.Sprintf("create user: failed to create user home dir: %s", err))
fullPath := ""
if filepath.Base(scope.Scope) != cleanedUserName && !u.Perm.Admin {
scope.Scope = filepath.Join(scope.Scope, cleanedUserName)
fullPath = filepath.Join(source.Path, scope.Scope)
} else {
fullPath = filepath.Join(source.Path, scope.Scope, cleanedUserName)
}
err := MakeUserDir(fullPath)
if err != nil {
return fmt.Errorf("create user: failed to create user home dir: %s", err)
}
// update scope to reflect new user home dir
u.Scopes[i] = scope
}
}
}
func cleanUsername(s string) string {
// Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ")
s = strings.Replace(s, "..", "", -1)
// Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash
s = invalidFilenameChars.ReplaceAllString(s, "-")
// Remove any multiple dashes caused by replacements above
s = dashes.ReplaceAllString(s, "-")
return s
return nil
}
-3
View File
@@ -15,7 +15,6 @@ import (
"golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/backend/users"
@@ -151,8 +150,6 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
user.Username = info.Username
user.Password = info.Password
files.MakeUserDirs(user)
err = store.Users.Save(user, true)
if err == errors.ErrExist {
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
+11 -7
View File
@@ -11,8 +11,8 @@ import (
jwt "github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
"github.com/gtsteffaniak/filebrowser/backend/storage"
"github.com/gtsteffaniak/filebrowser/backend/users"
)
@@ -56,7 +56,8 @@ func withHashFileHelper(fn handleFunc) handleFunc {
return status, fmt.Errorf("could not authenticate share request")
}
}
data.path = link.Path + "/" + path
data.path = strings.TrimSuffix(link.Path, "/") + "/" + strings.TrimPrefix(path, "/")
// Get file information with options
file, err := FileInfoFasterFunc(files.FileOptions{
Path: data.path,
@@ -68,7 +69,6 @@ func withHashFileHelper(fn handleFunc) handleFunc {
if err != nil {
return errToStatus(err), fmt.Errorf("error fetching share from server")
}
// Set the file info in the `data` object
data.raw = file
@@ -111,13 +111,17 @@ func withUserHelper(fn handleFunc) handleFunc {
return http.StatusInternalServerError, err
}
if config.Auth.Methods.ProxyAuth.CreateUser {
newUser := &users.User{Username: proxyUser}
settings.ApplyUserDefaults(newUser)
err := store.Users.Save(newUser, false)
hashpass, err := users.HashPwd(proxyUser)
if err != nil {
return http.StatusInternalServerError, err
}
data.user, err = store.Users.Get(proxyUser)
newUser := storage.CreateUser(users.User{
Username: proxyUser,
NonAdminEditable: users.NonAdminEditable{
Password: hashpass, // hashed password that can't actually be used
},
}, false)
data.user, err = store.Users.Get(newUser)
if err != nil {
return http.StatusInternalServerError, err
}
+8 -6
View File
@@ -192,9 +192,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
firstFileSource := splitFile[0]
firstFilePath := splitFile[1]
fileName := filepath.Base(firstFilePath)
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, firstFileSource)
if err != nil {
if err != nil && d.user.Username != "publicUser" {
return http.StatusForbidden, err
}
idx := files.GetIndex(firstFileSource)
@@ -231,9 +230,10 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
w.Header().Set("Cache-Control", "private")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
sizeInMB := estimatedSize / 1024 / 1024
// if larger than 100 megabytes, log it
if estimatedSize > 100*1024/1024 {
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, estimatedSize/1024/1024, fileName))
if sizeInMB > 100 {
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, sizeInMB, fileName))
}
// Stream file to response
_, err2 = io.Copy(w, fd)
@@ -267,9 +267,11 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
// Set headers for archive
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+fileName)
w.Header().Set("Content-Length", fmt.Sprintf("%d", estimatedSize))
sizeInMB := estimatedSize / 1024 / 1024
// if larger than 100 megabytes, log it
if estimatedSize > 100*1024/1024 {
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, estimatedSize/1024/1024, fileName))
if sizeInMB > 100 {
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, sizeInMB, fileName))
}
// Create archive and stream it
if extension == ".zip" {
-1
View File
@@ -387,7 +387,6 @@ func patchAction(ctx context.Context, action, src, dst string, d *requestContext
case "rename", "move":
idx := files.GetIndex(srcIndex)
srcPath := idx.MakeIndexPath(src)
fmt.Println("srcPath", srcPath)
fileInfo, err := files.FileInfoFaster(files.FileOptions{
Path: srcPath,
Source: srcIndex,
+4
View File
@@ -248,9 +248,13 @@ func ConvertToBackendScopes(scopes []users.SourceScope) ([]users.SourceScope, er
}
newScopes := []users.SourceScope{}
for _, scope := range scopes {
if scope.Scope == "" {
scope.Scope = "/"
}
// first check if its already a path name and keep it
source, ok := Config.Server.SourceMap[scope.Name]
if ok {
newScopes = append(newScopes, users.SourceScope{
Name: source.Path, // backend name is path
Scope: scope.Scope,
-1
View File
@@ -159,7 +159,6 @@ type UserDefaults struct {
SingleClick bool `json:"singleClick"`
Perm users.Permissions `json:"perm"`
Permissions users.Permissions `json:"permissions"`
Commands []string `json:"commands,omitempty"`
ShowHidden bool `json:"showHidden"`
DateFormat bool `json:"dateFormat"`
ThemeColor string `json:"themeColor"`
+34 -18
View File
@@ -8,6 +8,8 @@ import (
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
"github.com/gtsteffaniak/filebrowser/backend/utils"
@@ -93,24 +95,22 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
userFields := reflect.ValueOf(user).Elem() // Get struct value
for _, field := range fields {
correctedField := utils.CapitalizeFirst(field)
// Get the corresponding field using reflection
fieldValue := userFields.FieldByName(correctedField)
fieldValue := userFields.FieldByName(field)
if !fieldValue.IsValid() {
return fmt.Errorf("invalid field: %s", correctedField)
return fmt.Errorf("invalid field: %s", field)
}
// Ensure the field is settable
if !fieldValue.CanSet() {
return fmt.Errorf("cannot set value of field: %s", correctedField)
return fmt.Errorf("cannot set value of field: %s", field)
}
// Get the value to be stored
val := fieldValue.Interface()
// Update the database
if err := st.db.UpdateField(user, correctedField, val); err != nil {
return fmt.Errorf("failed to update user field: %s, error: %v", correctedField, err)
if err := st.db.UpdateField(user, field, val); err != nil {
return fmt.Errorf("failed to update user field: %s, error: %v", field, err)
}
}
return nil
@@ -136,6 +136,10 @@ func (st usersBackend) Save(user *users.User, changePass bool) error {
}
user.Scopes = adjustedScopes
err = files.MakeUserDirs(user)
if err != nil {
logger.Error(err.Error())
}
err = st.db.Save(user)
if err == storm.ErrAlreadyExists {
return errors.ErrExist
@@ -163,7 +167,7 @@ func checkRestrictedFields(existingUser *users.User, fields []string) error {
return fmt.Errorf("password is locked")
}
// Use reflection to get the field names of NonAdminEditable
editableFields := reflect.ValueOf(existingUser.NonAdminEditable)
editableFields := reflect.ValueOf(existingUser)
if editableFields.Kind() == reflect.Struct {
for i := 0; i < editableFields.NumField(); i++ {
fieldName := editableFields.Type().Field(i).Name
@@ -193,19 +197,31 @@ func parseFields(user *users.User, fields []string) ([]string, error) {
fields = append(fields, field.Name)
}
}
} else {
if slices.Contains(fields, "password") && user.Password != "" {
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
pass, err := users.HashPwd(user.Password)
if err != nil {
return fields, err
}
user.Password = pass
}
newfields := []string{}
for _, field := range fields {
newfields = append(newfields, utils.CapitalizeFirst(field))
}
if slices.Contains(newfields, "Password") && user.Password != "" {
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
pass, err := users.HashPwd(user.Password)
if err != nil {
return newfields, err
}
user.Password = pass
}
}
return fields, nil
if slices.Contains(newfields, "Scopes") {
newScopes, err := settings.ConvertToBackendScopes(user.Scopes)
if err == nil {
user.Scopes = newScopes
}
err = files.MakeUserDirs(user)
if err != nil {
logger.Error(err.Error())
}
}
return newfields, nil
}
func checkPassword(password string) error {
+1 -4
View File
@@ -8,7 +8,6 @@ import (
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/share"
@@ -110,7 +109,7 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
if userInfo.Username == "" || userInfo.Password == "" {
return errors.ErrInvalidRequestParams
}
logger.Debug(fmt.Sprintf("Creating user: %v %v\n", userInfo.Username, userInfo.Scopes))
logger.Debug(fmt.Sprintf("Creating user: %v %v", userInfo.Username, userInfo.Scopes))
settings.ApplyUserDefaults(newUser)
if asAdmin {
userInfo.Perm = settings.AdminPerms()
@@ -119,8 +118,6 @@ func CreateUser(userInfo users.User, asAdmin bool) error {
userInfo.Scopes = settings.Config.UserDefaults.DefaultScopes
}
// create new home directories
files.MakeUserDirs(newUser)
err := store.Users.Save(newUser, true)
if err != nil {
return err
-6
View File
@@ -1810,12 +1810,6 @@ const docTemplate = `{
"settings.UserDefaults": {
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "string"
}
},
"darkMode": {
"type": "boolean"
},
-6
View File
@@ -1799,12 +1799,6 @@
"settings.UserDefaults": {
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "string"
}
},
"darkMode": {
"type": "boolean"
},
-4
View File
@@ -134,10 +134,6 @@ definitions:
type: object
settings.UserDefaults:
properties:
commands:
items:
type: string
type: array
darkMode:
type: boolean
dateFormat:
+21
View File
@@ -1,9 +1,17 @@
package users
import (
"regexp"
"strings"
jwt "github.com/golang-jwt/jwt/v4"
)
var (
invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`)
dashes = regexp.MustCompile(`[\-]+`)
)
type AuthToken struct {
Key string `json:"key"`
Name string `json:"name"`
@@ -75,3 +83,16 @@ var PublicUser = User{
Api: false,
},
}
func CleanUsername(s string) string {
// Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ")
s = strings.Replace(s, "..", "", -1)
// Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash
s = invalidFilenameChars.ReplaceAllString(s, "-")
// Remove any multiple dashes caused by replacements above
s = dashes.ReplaceAllString(s, "-")
return s
}
+38 -15
View File
@@ -1,20 +1,43 @@
import { Browser, firefox, expect, Page } from "@playwright/test";
// Perform authentication and store auth state
async function globalSetup() {
const browser: Browser = await firefox.launch();
const context = await browser.newContext();
const page: Page = await context.newPage();
await page.goto("http://127.0.0.1/login");
await page.getByPlaceholder("Username").fill("admin");
await page.getByPlaceholder("Password").fill("admin");
await page.getByRole("button", { name: "Login" }).click();
await page.waitForURL("**/files/", { timeout: 100 });
let cookies = await context.cookies();
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.context().storageState({ path: "./loginAuth.json" });
await browser.close();
const browser: Browser = await firefox.launch();
const context = await browser.newContext();
const page: Page = await context.newPage();
await page.goto("http://127.0.0.1/login");
await page.getByPlaceholder("Username").fill("admin");
await page.getByPlaceholder("Password").fill("admin");
await page.getByRole("button", { name: "Login" }).click();
await page.waitForURL("**/files/", { timeout: 100 });
const cookies = await context.cookies();
expect(cookies.find((c) => c.name === "auth")?.value).toBeDefined();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.waitForURL("**/files/playwright-files", { timeout: 100 });
// Create a share
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="myfolder"]').click({ button: "right" });
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Share"]').click();
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /myfolder/');
await page.locator('button[aria-label="Share-Confirm"]').click();
await expect(page.locator("#share .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
const shareHash = await page.locator("#share .card-content table tbody tr:not(:has(th)) td").first().textContent();
if (!shareHash) {
throw new Error("Failed to retrieve shareHash");
}
// Store shareHash in localStorage
await page.evaluate((hash) => {
localStorage.setItem('shareHash', hash);
}, shareHash);
await context.storageState({ path: "./loginAuth.json" });
await browser.close();
}
export default globalSetup
export default globalSetup;
+2 -2
View File
@@ -1,5 +1,5 @@
import { adjustedData } from "./utils";
import { getApiPath, removePrefix,extractSourceFromPath } from "@/utils/url.js";
import { getApiPath, removePrefix, extractSourceFromPath } from "@/utils/url.js";
import { notify } from "@/notify";
// Fetch public share data
@@ -18,7 +18,7 @@ export async function fetchPub(path, hash, password = "") {
throw error;
}
let data = await response.json()
const adjusted = adjustedData(data, getApiPath(`share/${hash}${path}`));
const adjusted = adjustedData(data, removePrefix(getApiPath(`share/${hash}${path}`)));
return adjusted
}
+6 -8
View File
@@ -31,8 +31,7 @@
<script>
import { state, mutations, getters } from "@/store";
import { baseURL } from "@/utils/constants.js";
import { extractSourceFromPath, removePrefix, removeLeadingSlash } from "@/utils/url.js";
import { extractSourceFromPath, removeLeadingSlash } from "@/utils/url.js";
import Action from "@/components/Action.vue";
export default {
@@ -65,7 +64,6 @@ export default {
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = [];
let buildRef = this.base;
parts.forEach((element) => {
@@ -99,13 +97,13 @@ export default {
const result = extractSourceFromPath(getters.routePath());
if (getters.currentView() == "share") {
this.base = getters.sharePathBase();
this.path = removePrefix(getters.routePath(), this.base + "/");
this.path = getters.routePath(this.base);
} else {
this.path = decodeURIComponent(result.path);
}
this.base = baseURL;
if (state.serverHasMultipleSources) {
this.base = `${this.base}${result.source}/`;
this.base = "/";
if (state.serverHasMultipleSources) {
this.base = `${this.base}${result.source}/`;
}
}
},
updateGallerySize(event) {
+79
View File
@@ -251,6 +251,34 @@ export default {
if (state.serverHasMultipleSources) {
this.selectedSource = state.sources.current;
}
// Adjust contextmenu listener based on browser
if (state.isSafari) {
// For Safari, add touchstart or mousedown to open the context menu
this.$el.addEventListener("touchstart", this.openContextForSafari, {
passive: true,
});
this.$el.addEventListener("mousedown", this.openContextForSafari);
this.$el.addEventListener("touchmove", this.handleTouchMove);
// Also clear the timeout if the user clicks or taps quickly
this.$el.addEventListener("touchend", this.cancelContext);
this.$el.addEventListener("mouseup", this.cancelContext);
} else {
// For other browsers, use regular contextmenu
this.$el.addEventListener("contextmenu", this.openContext);
}
},
beforeUnmount() {
// If Safari, remove touchstart listener
if (state.isSafari) {
this.$el.removeEventListener("touchstart", this.openContextForSafari);
this.$el.removeEventListener("mousedown", this.openContextForSafari);
this.$el.removeEventListener("touchend", this.cancelContext);
this.$el.removeEventListener("mouseup", this.cancelContext);
this.$el.removeEventListener("touchmove", this.handleTouchMove);
} else {
this.$el.removeEventListener("contextmenu", this.openContext);
}
},
computed: {
showOptions() {
@@ -319,6 +347,57 @@ export default {
},
},
methods: {
openContext(event) {
event.preventDefault();
event.stopPropagation();
mutations.showHover({
name: "ContextMenu",
props: {
posX: event.clientX,
posY: event.clientY,
},
});
},
cancelContext() {
if (this.contextTimeout) {
clearTimeout(this.contextTimeout);
this.contextTimeout = null;
}
this.isLongPress = false;
},
openContextForSafari(event) {
this.cancelContext(); // Clear any previous timeouts
this.isLongPress = false; // Reset state
this.isSwipe = false; // Reset swipe detection
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
// Start the long press detection
this.contextTimeout = setTimeout(() => {
if (!this.isSwipe) {
this.isLongPress = true;
event.preventDefault(); // Suppress Safari's callout menu
this.openContext(event); // Open the custom context menu
}
}, 500); // Long press delay (adjust as needed)
},
handleTouchMove(event) {
const touch = event.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchStartX);
const deltaY = Math.abs(touch.clientY - this.touchStartY);
// Set a threshold for movement to detect a swipe
const movementThreshold = 10; // Adjust as needed
if (deltaX > movementThreshold || deltaY > movementThreshold) {
this.isSwipe = true;
this.cancelContext(); // Cancel long press if swipe is detected
}
},
handleTouchEnd() {
this.cancelContext(); // Clear timeout
this.isSwipe = false; // Reset swipe state
},
updateSource(event) {
this.selectedSource = event.target.value;
this.submit();
@@ -96,7 +96,6 @@ import { fromNow } from "@/utils/moment";
import { filesApi } from "@/api";
import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { baseURL } from "@/utils/constants";
import { router } from "@/router";
import { url } from "@/utils";
import Icon from "@/components/Icon.vue";
@@ -233,7 +232,7 @@ export default {
window.location.href = this.getRelative(path);
},
getUrl() {
return baseURL.slice(0, -1) + this.url;
return url.removePrefix(this.url);
},
onRightClick(event) {
event.preventDefault(); // Prevent default context menu
@@ -433,8 +432,7 @@ export default {
},
open() {
location.hash = state.req.items[this.index].name;
const newurl = url.removePrefix(this.url);
router.push({ path: newurl });
router.push({ path: this.url });
},
},
};
+1 -1
View File
@@ -8,6 +8,7 @@
<p>{{ $t("prompts.newFileMessage") }}</p>
<input
class="input input--block"
aria-label="FileName Field"
v-focus
type="text"
@keyup.enter="submit"
@@ -82,7 +83,6 @@ export default {
} catch (error) {
notify.showError(error);
}
},
},
};
+7 -2
View File
@@ -11,7 +11,7 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="currentPrompt.action"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
@@ -29,7 +29,7 @@
</div>
</template>
<script>
import { getters } from "@/store"; // Import your custom store
import { getters, mutations } from "@/store"; // Import your custom store
export default {
name: "replace",
@@ -38,5 +38,10 @@ export default {
return getters.currentPrompt(); // Access the getter directly from the store
},
},
methods: {
closeHovers() {
mutations.closeHovers();
},
},
};
</script>
+5 -4
View File
@@ -3,7 +3,7 @@
<div class="card-title">
<h2>{{ $t("buttons.share") }}</h2>
</div>
<div aria-label="share-paths" class="searchContext">Path: {{ subpath }}</div>
<div aria-label="share-path" class="searchContext">Path: {{ subpath }}</div>
<p>
Note: anyone who has access to the link (and optional password) can access the
shared files. There is no requirement to be logged in.
@@ -115,7 +115,7 @@
<button
class="button button--flat button--blue"
@click="submit"
:aria-label="$t('buttons.share')"
aria-label="Share-Confirm"
:title="$t('buttons.share')"
>
{{ $t("buttons.share") }}
@@ -131,6 +131,7 @@ import { shareApi, publicApi } from "@/api";
import { fromNow } from "@/utils/moment";
import { removePrefix } from "@/utils/url";
import Clipboard from "clipboard";
import { baseURL } from "@/utils/constants";
export default {
name: "share",
@@ -178,7 +179,7 @@ export default {
},
async beforeMount() {
try {
let path = getters.routePath("files");
let path = getters.routePath(baseURL);
if (state.isSearchActive) {
path = state.selected[0].path;
this.source = state.selected[0].source;
@@ -270,7 +271,7 @@ export default {
},
buildDownloadLink(share) {
share.source = this.source;
share.path = this.subpath;
share.path = "/";
return publicApi.getDownloadURL(share);
},
sort() {
@@ -94,11 +94,6 @@ export default {
if (conflict) {
mutations.showHover({
name: "replace",
action: async (event) => {
event.preventDefault();
mutations.closeHovers();
await upload.handleFiles(uploadFiles, path, false);
},
confirm: async (event) => {
event.preventDefault();
mutations.closeHovers();
+2 -30
View File
@@ -47,7 +47,7 @@
</div>
<!-- Section for logged-in users -->
<div v-if="loginCheck && !disableUsedPercentage" class="sidebar-scroll-list">
<div v-if="loginCheck" class="sidebar-scroll-list">
<div class="sources card">
<span>Sources</span>
<div class="inner-card">
@@ -83,16 +83,8 @@
<script>
import * as auth from "@/utils/auth";
import {
signup,
disableExternal,
disableUsedPercentage,
noAuth,
loginPage,
} from "@/utils/constants";
import { filesApi } from "@/api";
import { signup, disableExternal, noAuth, loginPage } from "@/utils/constants";
import ProgressBar from "@/components/ProgressBar.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { state, getters, mutations } from "@/store"; // Import your custom store
export default {
@@ -135,27 +127,7 @@ export default {
}
},
},
mounted() {
if (this.loginCheck) {
this.updateUsage();
}
},
methods: {
async updateUsage() {
if (!disableUsedPercentage) {
if (state.sources.info === undefined) {
return;
}
for (const source of Object.keys(state.sources.info)) {
let usage = await filesApi.usage(source);
let sourceInfo = state.sources.info[source];
sourceInfo.used = getHumanReadableFilesize(usage.used);
sourceInfo.total = getHumanReadableFilesize(usage.total);
sourceInfo.usedPercentage = Math.round((usage.used / usage.total) * 100);
mutations.updateSource(source, sourceInfo);
}
}
},
checkLogin() {
return getters.isLoggedIn() && !getters.routePath().startsWith("/share");
},
+5 -8
View File
@@ -187,10 +187,11 @@ main .spinner .bounce2 {
}
#previewer .preview .info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 1.5em;
color: #fff;
}
@@ -247,10 +248,6 @@ main .spinner .bounce2 {
padding: 0.4em;
}
#previewer>button:first-of-type {
left: 0.5em;
}
#previewer>button:last-of-type {
right: 0.5em;
}
+1 -1
View File
@@ -139,7 +139,7 @@ router.beforeResolve(async (to, from, next) => {
// Handle auth requirements
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (state != null && state.user != null && !('username' in state.user)) {
if (state != null && state.user != null && !state.user.username){
await validateLogin();
}
+26 -26
View File
@@ -1,10 +1,11 @@
import { removePrefix } from "@/utils/url.js";
import { getFileExtension } from "@/utils/files.js";
import { state } from "./state.js";
import { mutations } from "./mutations.js";
import { noAuth,baseURL } from "@/utils/constants.js";
import { noAuth } from "@/utils/constants.js";
import { getTypeInfo } from "@/utils/mimetype";
export const getters = {
previewType: () => getTypeInfo(state.req.type).simpleType,
isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
currentHash: () => state.route.hash.replace("#", ""),
isMobile: () => state.isMobile,
@@ -21,22 +22,9 @@ export const getters = {
if (noAuth) {
return true
}
if (state.user !== null && state.user?.username != undefined && state.user?.username != "publicUser") {
if (state.user !== null && state.user?.username != "" && state.user?.username != "publicUser") {
return true;
}
const userData = localStorage.getItem("userData");
if (userData == undefined) {
return false;
}
try {
const userInfo = JSON.parse(userData);
if (userInfo.username != "publicUser") {
mutations.setCurrentUser(userInfo);
return true;
}
} catch (error) {
return false;
}
return false
},
isAdmin: () => state.user.perm?.admin == true,
@@ -94,34 +82,46 @@ export const getters = {
return { dirs, files };
},
isSidebarVisible: () => {
const currentView = getters.currentView();
let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
if (getters.currentView() == "settings") {
if (currentView == "settings") {
visible = !getters.isMobile();
}
if (getters.currentView() == "share") {
if (currentView == "share") {
visible = false
}
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
visible = false;
}
if (currentView == "preview") {
const contentType = getters.previewType()
if (
contentType == "audio" ||
contentType == "video" ||
contentType == "image" ||
contentType == "pdf"
) {
visible = false
}
}
if (
getters.currentView() == "editor" ||
getters.currentView() == "preview" ||
getters.currentView() == "onlyOfficeEditor"
currentView == "editor" ||
currentView == "onlyOfficeEditor"
) {
visible = false;
}
return visible
},
isStickySidebar: () => {
let sticky = state.user?.stickySidebar
if (getters.currentView() == "settings") {
let sticky = state.user?.stickySidebar;
const currentView = getters.currentView();
if (currentView == "settings") {
sticky = true
}
if (getters.currentView() == null && !getters.isLoading() && getters.isShare() ) {
if (currentView == null && !getters.isLoading() && getters.isShare() ) {
sticky = true
}
if (getters.isMobile() || getters.currentView() == "preview") {
if (getters.isMobile()) {
sticky = false
}
return sticky
@@ -142,7 +142,7 @@ export const getters = {
// Step 1: Split the path by '/'
let parts = urlPath.split("/");
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
return baseURL + "share/"+ parts[1];
return "/share/"+ parts[1] + "/";
},
currentView: () => {
const pathname = getters.routePath()
+1 -7
View File
@@ -130,16 +130,14 @@ export const mutations = {
},
setCurrentUser: (value) => {
try {
localStorage.setItem("userData", undefined);
// If value is null or undefined, emit state change and exit early
if (!value) {
state.user = value;
emitStateChanged();
return;
}
mutations.setSources(value);
if (value.username != "publicUser") {
localStorage.setItem("userData", JSON.stringify(value));
mutations.setSources(value);
}
// Ensure locale exists and is valid
if (!value.locale) {
@@ -189,7 +187,6 @@ export const mutations = {
emitStateChanged();
},
updateCurrentUser: (value) => {
localStorage.setItem("userData", undefined);
// Ensure the input is a valid object
if (typeof value !== "object" || value === null) return;
@@ -220,9 +217,6 @@ export const mutations = {
usersApi.update(state.user, Object.keys(value));
}
if (state.user.username != "publicUser") {
localStorage.setItem("userData", JSON.stringify(state.user));
}
// Emit state change event
emitStateChanged();
},
+1
View File
@@ -17,6 +17,7 @@ export const state = reactive({
info: {},
},
user: {
username: "",
quickDownloadEnabled: false,
gallarySize: 0,
singleClick: false,
+15 -4
View File
@@ -1,9 +1,9 @@
import { mutations, getters } from "@/store";
import { mutations, getters, state } from "@/store";
import router from "@/router";
import { usersApi } from "@/api";
import { usersApi,filesApi } from "@/api";
import { getApiPath } from "@/utils/url.js";
import { recaptcha, loginPage } from "@/utils/constants";
import { recaptcha, loginPage, disableUsedPercentage } from "@/utils/constants";
import { getHumanReadableFilesize } from "@/utils/filesizes";
export async function setNewToken(token) {
document.cookie = `auth=${token}; path=/`;
@@ -18,6 +18,17 @@ export async function validateLogin() {
} catch (error) {
console.log("Error validating login");
}
if (!disableUsedPercentage) {
for (const source of Object.keys(state.sources.info)) {
let usage = await filesApi.usage(source);
let sourceInfo = state.sources.info[source];
sourceInfo.used = getHumanReadableFilesize(usage.used);
sourceInfo.total = getHumanReadableFilesize(usage.total);
sourceInfo.usedPercentage = Math.round((usage.used / usage.total) * 100);
mutations.updateSource(source, sourceInfo);
}
}
return getters.isLoggedIn()
}
+5 -9
View File
@@ -103,32 +103,28 @@ export function scanFiles(dt) {
}
export async function handleFiles(files, base, overwrite = false) {
console.log("Uploading files", files);
let blockUpdates = false;
let c = 0
let count = files.length
for (const file of files) {
c += 1
const id = state.upload.id;
let path = base;
if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath);
} else {
path += url.encodeRFC5987ValueChars(file.name);
}
let path = url.removeTrailingSlash(base) + "/" +file.name;
if (file.type == "directory") {
path += "/";
}
const item = {
id,
path,
id: id,
path: path,
file: file.file, // Ensure `file.file` is the Blob or File
overwrite,
};
let last = 0;
notify.showPopup("success", `(${c} of ${count}) Uploading ${file.name}`, false);
console.log(`Uploading ${file.name}`, item);
await filesApi.post(
item.path,
item.file,
+6 -3
View File
@@ -115,14 +115,17 @@ export function base64Encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
// expect url to include /files/ prefix
export function extractSourceFromPath(url) {
let source;
let path = removePrefix(url, 'files');
let path = url;
if (state.serverHasMultipleSources) {
source = path.split('/')[1];
path = removePrefix(path, source);
source = path.split('/')[2];
path = removePrefix(path, `/files/${source}`);
} else {
source = state.sources.current;
path = removePrefix(path, '/files');
}
return { source, path };
}
+14 -1
View File
@@ -1,10 +1,12 @@
import { describe, it, expect, vi } from 'vitest';
import { removePrefix, extractSourceFromPath } from './url.js';
import { removePrefix, extractSourceFromPath, getApiPath } from './url.js';
describe('testurl', () => {
it('url prefix', () => {
let tests = [
{input: "/files/files", trimArg: "/files/",expected: "/files",},
{input: "/files/share/something/", trimArg: "files", expected:"/share/something/"},
{input: "test/iscool/", trimArg: "test",expected:"/iscool/"},
{input: "test",trimArg: "",expected:"/test"},
{input: "/test", trimArg: "test",expected:"/"},
@@ -19,6 +21,17 @@ describe('testurl', () => {
});
describe('getapipath', () => {
it('url prefix', () => {
let tests = [
{input: "/share/to/thing", expected: "/files/share/to/thing",},
]
for (let test of tests) {
expect(getApiPath(test.input)).toEqual(test.expected);
}
});
});
describe('extractSourceFromPath', () => {
it('single source extract', async () => {
vi.doMock("@/store", () => {
+13 -8
View File
@@ -29,6 +29,7 @@ import { state, mutations, getters } from "@/store";
import { url } from "@/utils";
import { notify } from "@/notify";
import router from "@/router";
import { baseURL } from "@/utils/constants";
export default {
name: "files",
@@ -70,7 +71,6 @@ export default {
$route: "fetchData",
reload(value) {
if (value) {
console.log("Reloading");
this.fetchData();
}
},
@@ -101,15 +101,20 @@ export default {
}
},
async fetchData() {
// lets redirect if multiple sources and user went to /files/
if (
state.serverHasMultipleSources &&
(getters.routePath() === "/files/" || getters.routePath() === "/files")
) {
router.push(`/files/${state.sources.current}`);
if (!getters.isLoggedIn()) {
return;
}
const routePath = url.removeTrailingSlash(getters.routePath(`${baseURL}files`));
const rootRoute =
routePath == "/files" ||
routePath == "/files/" ||
routePath == "" ||
routePath == "/";
// lets redirect if multiple sources and user went to /files/
if (state.serverHasMultipleSources && rootRoute) {
router.push(`${routePath}/${state.sources.current}`);
return;
}
this.lastHash = "";
// Set loading to true and reset the error.
mutations.setLoading("files", true);
+21 -21
View File
@@ -298,7 +298,6 @@ export default {
this.colunmsResize();
// Add the needed event listeners to the window and document.
window.addEventListener("keydown", this.keyEvent);
window.addEventListener("scroll", this.scrollEvent);
window.addEventListener("resize", this.windowsResize);
window.addEventListener("click", this.clickClear);
window.addEventListener("keyup", this.clearCtrKey);
@@ -307,7 +306,9 @@ export default {
// Adjust contextmenu listener based on browser
if (state.isSafari) {
// For Safari, add touchstart or mousedown to open the context menu
this.$el.addEventListener("touchstart", this.openContextForSafari);
this.$el.addEventListener("touchstart", this.openContextForSafari, {
passive: true,
});
this.$el.addEventListener("mousedown", this.openContextForSafari);
this.$el.addEventListener("touchmove", this.handleTouchMove);
@@ -316,10 +317,10 @@ export default {
this.$el.addEventListener("mouseup", this.cancelContext);
} else {
// For other browsers, use regular contextmenu
window.addEventListener("contextmenu", this.openContext);
this.$el.addEventListener("contextmenu", this.openContext);
}
// if safari , make sure click and hold opens context menu, but not for any other browser
if (!state.user.perm?.create) return;
if (!state.user.perm?.modify) return;
this.$el.addEventListener("dragenter", this.dragEnter);
this.$el.addEventListener("dragleave", this.dragLeave);
this.$el.addEventListener("drop", this.drop);
@@ -327,7 +328,6 @@ export default {
beforeUnmount() {
// Remove event listeners before destroying this page.
window.removeEventListener("keydown", this.keyEvent);
window.removeEventListener("scroll", this.scrollEvent);
window.removeEventListener("resize", this.windowsResize);
// If Safari, remove touchstart listener
if (state.isSafari) {
@@ -337,7 +337,7 @@ export default {
this.$el.removeEventListener("mouseup", this.cancelContext);
this.$el.removeEventListener("touchmove", this.handleTouchMove);
} else {
window.removeEventListener("contextmenu", this.openContext);
this.$el.removeEventListener("contextmenu", this.openContext);
}
},
methods: {
@@ -517,7 +517,7 @@ export default {
}
},
keyEvent(event) {
if (state.isSearchActive || getters.currentView() !== "files") {
if (state.isSearchActive || getters.currentView() !== "listingView") {
return;
}
const { key, ctrlKey, metaKey, which } = event;
@@ -553,6 +553,7 @@ export default {
}
return;
}
// Handle key events using a switch statement
switch (key) {
case "Enter":
@@ -791,26 +792,25 @@ export default {
}
let files = await upload.scanFiles(dt);
const folderUpload = Boolean(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 path = getters.routePath();
if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
let response = await filesApi.fetchFiles(decodeURIComponent(el.__vue__.url));
path = el.__vue__.url;
items = state.req.items;
items = response.items;
}
const uploadFiles = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
uploadFiles.push({
file,
name: file.name,
source: state.req.source,
size: file.size,
});
}
const conflict = upload.checkConflict(uploadFiles, items);
try {
+10 -13
View File
@@ -1,10 +1,9 @@
<template>
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<div class="preview">
<ExtendedImage v-if="getSimpleType(req.type) == 'image'" :src="raw">
</ExtendedImage>
<ExtendedImage v-if="previewType == 'image'" :src="raw"> </ExtendedImage>
<audio
v-else-if="getSimpleType(req.type) == 'audio'"
v-else-if="previewType == 'audio'"
ref="player"
:src="raw"
controls
@@ -12,7 +11,7 @@
@play="autoPlay = true"
></audio>
<video
v-else-if="getSimpleType(req.type) == 'video'"
v-else-if="previewType == 'video'"
ref="player"
:src="raw"
controls
@@ -29,11 +28,7 @@
/>
</video>
<object
v-else-if="getSimpleType(req.type) == 'pdf'"
class="pdf"
:data="raw"
></object>
<object v-else-if="previewType == 'pdf'" class="pdf" :data="raw"></object>
<div v-else class="info">
<div class="title">
<i class="material-icons">feedback</i>
@@ -90,7 +85,6 @@ import url from "@/utils/url.js";
import throttle from "@/utils/throttle";
import ExtendedImage from "@/components/files/ExtendedImage.vue";
import { state, getters, mutations } from "@/store";
import { getTypeInfo } from "@/utils/mimetype";
import { getFileExtension } from "@/utils/files";
import { convertToVTT } from "@/utils/subtitles";
@@ -117,6 +111,12 @@ export default {
};
},
computed: {
sidebarShowing() {
return getters.isSidebarVisible();
},
previewType() {
return getters.previewType();
},
raw() {
return filesApi.getDownloadURL(state.req.source, state.req.path, true);
},
@@ -184,9 +184,6 @@ export default {
}
return subs;
},
getSimpleType(mimetype) {
return getTypeInfo(mimetype).simpleType;
},
deleteFile() {
this.currentPrompt = {
name: "delete",
+29
View File
@@ -0,0 +1,29 @@
import { test as base, expect, Page } from "@playwright/test";
export const test = base.extend<{ checkForErrors: () => void }>({
checkForErrors: async ({ page }, use) => {
const { checkForErrors } = setupErrorTracking(page);
await use(checkForErrors);
},
});
// Error tracking function
export function setupErrorTracking(page: Page) {
const errors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") {
errors.push(message.text());
}
});
return {
checkForErrors: () => {
if (errors.length > 0) {
console.error("Console Errors Detected:", errors);
}
expect(errors).toHaveLength(0);
},
};
}
export { expect };
@@ -1,16 +1,18 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "../test-setup";
test("blob file preview", async ({ page, context }) => {
test("blob file preview", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file.tar.gz"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
await page.locator('button[title="Close"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
// Check for console errors
checkForErrors();
});
test("text file editor", async ({ page, context }) => {
test("text file editor", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@@ -20,9 +22,11 @@ test("text file editor", async ({ page, context }) => {
expect(firstLineText).toBe('test file for playwright');
await page.locator('button[title="Close"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
// Check for console errors
checkForErrors();
});
test("navigate folders", async ({ page, context }) => {
test("navigate folders", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
@@ -34,21 +38,6 @@ test("navigate folders", async ({ page, context }) => {
await page.locator('a[aria-label="gray-sample.jpg"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="gray-sample.jpg"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
});
test("navigating images", async ({ page, context }) => {
await page.goto("/files/playwright-files/myfolder/testdata/20130612_142406.jpg");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - 20130612_142406.jpg");
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
await page.mouse.move(100, 100);
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Next"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
await page.locator('button[aria-label="Next"]').waitFor({ state: 'hidden' });
await page.mouse.move(100, 100);
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
//await page.locator('button[aria-label="Next"]').click();
// went to next image
//await expect(page).toHaveTitle("Graham's Filebrowser - Files - IMG_2578.JPG");
// Check for console errors
checkForErrors();
});
+10 -52
View File
@@ -1,55 +1,13 @@
import { test, expect } from "@playwright/test";
test("blob file preview", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file.tar.gz"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
await page.locator('button[title="Close"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
test("Create first new file", async ({ page, context }) => {
await page.goto("/");
await expect(page.locator('#listingView .message > span')).toHaveText('It feels lonely here...');
await page.locator('#listingView').click({ button: "right" });
await page.locator('button[aria-label="New file"]').click();
await page.locator('input[aria-label="FileName Field"]').fill('test.txt');
await page.locator('button[aria-label="Create"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - test.txt");
await page.locator('button[aria-label="Close"]').click();
await expect(page.locator('#listingView .file-items')).toHaveCount(1);
});
test("text file editor", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="copyme.txt"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - copyme.txt");
const firstLineText = await page.locator('.ace_text-layer .ace_line').first().textContent();
expect(firstLineText).toBe('test file for playwright');
await page.locator('button[title="Close"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
});
test("navigate folders", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="myfolder"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
await page.locator('a[aria-label="testdata"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="testdata"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
await page.locator('a[aria-label="gray-sample.jpg"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="gray-sample.jpg"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
});
test("navigating images", async ({ page, context }) => {
await page.goto("/files/myfolder/testdata/20130612_142406.jpg");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - 20130612_142406.jpg");
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
await page.mouse.move(100, 100);
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Next"]').click();
// went to next image
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
await page.locator('button[aria-label="Next"]').waitFor({ state: 'hidden' });
await page.mouse.move(100, 100);
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
//await page.locator('button[aria-label="Next"]').click();
// went to next image
//await expect(page).toHaveTitle("Graham's Filebrowser - Files - IMG_2578.JPG");
});
+25 -7
View File
@@ -1,6 +1,7 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "../test-setup";
test("info from listing", async ({ page, context }) => {
test("info from listing", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
@@ -9,9 +10,10 @@ test("info from listing", async ({ page, context }) => {
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Info"]').click();
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
checkForErrors();
});
test("info from search", async ({ page, context }) => {
test("info from search", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
@@ -22,9 +24,22 @@ test("info from search", async ({ page, context }) => {
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Info"]').click();
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
checkForErrors();
})
test("2x copy from listing to new folder", async ({ page, context }) => {
test("open from search", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
await page.locator('#main-input').fill('file.tar.gz');
await expect(page.locator('#result-list')).toHaveCount(1);
await page.locator('li[aria-label="file.tar.gz"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
await expect(page.locator('#previewer')).toContainText('Preview is not available for this file.');
checkForErrors();
})
test("2x copy from listing to new folder", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@@ -70,9 +85,10 @@ test("2x copy from listing to new folder", async ({ page, context }) => {
await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/newfolder/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
checkForErrors();
})
test("delete file", async ({ page, context }) => {
test("delete file", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="deleteme.txt"]').waitFor({ state: 'visible' });
@@ -86,9 +102,11 @@ test("delete file", async ({ page, context }) => {
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Deleted item successfully! reloading...");
checkForErrors();
})
test("delete nested file prompt", async ({ page, context }) => {
test("delete nested file prompt", async({ page, checkForErrors, context }) => {
await page.goto("/files/playwright-files/folder%23hash/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
@@ -98,5 +116,5 @@ test("delete nested file prompt", async ({ page, context }) => {
await page.locator('button[aria-label="Delete"]').click();
await expect(page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/folder#hash/file#.sh');
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh');
checkForErrors();
})
+1 -1
View File
@@ -85,6 +85,6 @@ vi.mock('@/store', () => {
vi.mock('@/utils/constants', () => {
return {
baseURL: "http://example.com",
baseURL: "/files/",
};
});
+16 -15
View File
@@ -1,19 +1,20 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "../test-setup";
test("navigate with hash in file name", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="folder#hash"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="folder#hash"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file#.sh");
await expect(page.locator('.topTitle')).toHaveText('file#.sh');
test("navigate with hash in file name", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="folder#hash"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="folder#hash"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file#.sh");
await expect(page.locator('.topTitle')).toHaveText('file#.sh');
checkForErrors()
})
test("breadcrumbs navigation checks", async ({ page }) => {
test("breadcrumbs navigation checks", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", "/playwright-files/");
@@ -42,11 +43,10 @@ test("breadcrumbs navigation checks", async ({ page }) => {
expect(spanChildrenCount).toBe(1);
breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-files"] a')
await expect(breadCrumbLink).toHaveText("files");
checkForErrors();
});
test("navigate from search item", async ({ page }) => {
test("navigate from search item", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
@@ -55,4 +55,5 @@ test("navigate from search item", async ({ page }) => {
await page.locator('li[aria-label="for testing.md"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - for testing.md");
await expect(page.locator('.topTitle')).toHaveText('for testing.md');
checkForErrors()
});
+25
View File
@@ -0,0 +1,25 @@
import { test, expect } from "../test-setup";
test("breadcrumbs navigation checks", async ({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHash = await page.evaluate(() => localStorage.getItem('shareHash'));
if (shareHash == "") {
throw new Error("Share hash not found in localStorage");
}
await page.goto("/share/" + shareHash);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - myfolder");
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", `/share/${shareHash}/`);
await page.dblclick('a[aria-label="testdata"]');
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
// Ensure no <span> children exist directly under .breadcrumbs (ie no breadcrumbs paths)
let spanChildrenCount = await page.locator('.breadcrumbs > span').count();
await page.waitForSelector('.breadcrumbs');
expect(spanChildrenCount).toBe(1);
checkForErrors();
});
+7 -3
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { test, expect } from "../test-setup";
test("sidebar links", async ({ page }) => {
test("sidebar links", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
// Verify the page title
@@ -17,9 +17,11 @@ test("sidebar links", async ({ page }) => {
// Assert that the <a> does not contain the text 'Help'
await expect(credits.locator("span").locator("a")).not.toHaveText('Help');
// Check for console errors
checkForErrors();
});
test("adjusting theme colors", async ({ page }) => {
test("adjusting theme colors", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
const originalPrimaryColor = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
@@ -38,4 +40,6 @@ test("adjusting theme colors", async ({ page }) => {
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
});
await expect(newPrimaryColor).toBe('#9b59b6');
// Check for console errors
checkForErrors();
});
+23 -23
View File
@@ -1,25 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node10",
"strict": true,
"sourceMap": true,
"noImplicitReturns": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["vite/client", "@intlify/unplugin-vue-i18n/messages"],
"paths": {
"@/*": ["./src/*"]
},
"checkJs": false, // check later
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node10",
"strict": true,
"sourceMap": true,
"noImplicitReturns": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["vite/client", "@intlify/unplugin-vue-i18n/messages"],
"paths": {
"@/*": ["./src/*"]
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.vue", "tests/auth.spec.ts"],
"exclude": ["node_modules", "dist"]
}
"checkJs": false, // check later
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.vue", "tests/auth.spec.ts"],
"exclude": ["node_modules", "dist"]
}