Compare commits
4
Commits
main
...
v0.6.2-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7753dfab7 | ||
|
|
7b781971a7 | ||
|
|
45bbeb2776 | ||
|
|
f8a507f340 |
No files matched your search
@@ -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,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/
|
||||
|
||||
File renamed without changes.
+2
-2
@@ -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,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. */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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,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
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1810,12 +1810,6 @@ const docTemplate = `{
|
||||
"settings.UserDefaults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -1799,12 +1799,6 @@
|
||||
"settings.UserDefaults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -134,10 +134,6 @@ definitions:
|
||||
type: object
|
||||
settings.UserDefaults:
|
||||
properties:
|
||||
commands:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
darkMode:
|
||||
type: boolean
|
||||
dateFormat:
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ export const state = reactive({
|
||||
info: {},
|
||||
},
|
||||
user: {
|
||||
username: "",
|
||||
quickDownloadEnabled: false,
|
||||
gallarySize: 0,
|
||||
singleClick: false,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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();
|
||||
})
|
||||
@@ -85,6 +85,6 @@ vi.mock('@/store', () => {
|
||||
|
||||
vi.mock('@/utils/constants', () => {
|
||||
return {
|
||||
baseURL: "http://example.com",
|
||||
baseURL: "/files/",
|
||||
};
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user