More bugfixes 0.8.2 (#1122)
This commit is contained in:
1 parent
70933aea36
commit
509633b913
67 files changed
+783
-310
No files matched your search
@@ -6,16 +6,24 @@ All notable changes to this project will be documented in this file. For commit
|
||||
|
||||
**New Features**:
|
||||
- added `source.config.denyByDefault` configuration to enable a deny-by-default access rule. A source enabled with this will deny access unless an "allow" rule was specifically created. (Similar to creating a root-level denyAll rule)
|
||||
- allow oidc user source access and permission based on username and groups is fulfilled by denyByDefault source with access rules https://github.com/gtsteffaniak/filebrowser/issues/824
|
||||
- "open parent folder" in context menu and search results https://github.com/gtsteffaniak/filebrowser/issues/1121
|
||||
- added friendly "share not found" page.
|
||||
|
||||
**Notes**:
|
||||
- 8.0 ffmpeg version bundled with docker
|
||||
- go 1.25 upgrade with green tea GC enabled
|
||||
- totp secrets accept non-secure strings, only throwing warning
|
||||
- adjusted download limit so it also counts viewing text "content" of files (like in editor). You can also "disable file viewing" to stop the editor from showing. lower quality file image previews are not counted as downloads.
|
||||
- updated invalid share message to be more clear https://github.com/gtsteffaniak/filebrowser/issues/1120
|
||||
|
||||
**BugFixes**:
|
||||
- fixed /public/static routes issue
|
||||
- shares redirect to login - https://github.com/gtsteffaniak/filebrowser/issues/1109
|
||||
- some static assets not available to anonymous user - https://github.com/gtsteffaniak/filebrowser/issues/1102
|
||||
- more safari style issues https://github.com/gtsteffaniak/filebrowser/issues/1110
|
||||
- fix public share download issues https://github.com/gtsteffaniak/filebrowser/issues/1118 https://github.com/gtsteffaniak/filebrowser/issues/1089
|
||||
- fixed disable file viewer setting and enforced on backend
|
||||
|
||||
## v0.8.1-beta
|
||||
|
||||
|
||||
+17
-4
@@ -2,10 +2,13 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// StorageBackend is a storage backend for auth storage.
|
||||
@@ -35,9 +38,19 @@ func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptionKey, err = base64.StdEncoding.DecodeString(settings.Config.Auth.TotpSecret)
|
||||
if err != nil {
|
||||
logger.Warning("failed to decode TOTP secret, using default key. This is insecure and should be fixed.")
|
||||
if settings.Config.Auth.TotpSecret != "" {
|
||||
key, err := base64.StdEncoding.DecodeString(settings.Config.Auth.TotpSecret)
|
||||
keyLen := len(key)
|
||||
|
||||
if err == nil && (keyLen == 16 || keyLen == 24 || keyLen == 25) {
|
||||
// Use the user-provided key if it's valid
|
||||
encryptionKey = key
|
||||
} else {
|
||||
// Otherwise, derive the key from the provided secret string
|
||||
log.Println("[WARN] totpSecret is not a valid Base64 encoded key. Deriving a key from it. For better security, generate a secret with 'openssl rand -base64 32'.")
|
||||
salt := []byte{0xda, 0x90, 0x45, 0xc3, 0x06, 0xb5, 0x99, 0x9f, 0xb6, 0xae, 0xfc, 0x14, 0xef, 0x27, 0x6e, 0x6a}
|
||||
encryptionKey = pbkdf2.Key([]byte(settings.Config.Auth.TotpSecret), salt, 4096, 32, sha256.New)
|
||||
}
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import "sync"
|
||||
type CommonShare struct {
|
||||
//AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
AllowUpload bool `json:"allowUpload,omitempty"`
|
||||
DisablingFileViewer bool `json:"disableFileViewer,omitempty"`
|
||||
DisableFileViewer bool `json:"disableFileViewer,omitempty"`
|
||||
DownloadsLimit int `json:"downloadsLimit,omitempty"`
|
||||
ShareTheme string `json:"shareTheme,omitempty"`
|
||||
DisableAnonymous bool `json:"disableAnonymous,omitempty"`
|
||||
@@ -26,7 +26,6 @@ type CommonShare struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"downloadURL,omitempty"`
|
||||
DisableShareCard bool `json:"disableShareCard,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
|
||||
@@ -71,17 +71,6 @@ func NewStorage(back StorageBackend, usersStore *users.Storage) *Storage {
|
||||
}
|
||||
}
|
||||
|
||||
// setUsername populates the Link.Username from the users store using UserID, if available.
|
||||
func (s *Storage) setUsername(l *Link) {
|
||||
if l == nil || s.users == nil {
|
||||
return
|
||||
}
|
||||
user, err := s.users.Get(l.UserID)
|
||||
if err == nil && user != nil {
|
||||
l.Username = user.Username
|
||||
}
|
||||
}
|
||||
|
||||
// All wraps StorageBackend.All and handles expiry.
|
||||
func (s *Storage) All() ([]*Link, error) {
|
||||
links, err := s.back.All()
|
||||
@@ -92,10 +81,7 @@ func (s *Storage) All() ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// warm cache: avoid DB calls under lock
|
||||
for _, l := range filtered {
|
||||
s.setUsername(l)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
@@ -121,10 +107,7 @@ func (s *Storage) FindByUserID(id uint) ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// inject username outside lock
|
||||
for _, l := range filtered {
|
||||
s.setUsername(l)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
@@ -166,7 +149,7 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.setUsername(link)
|
||||
|
||||
s.shareCache[hash] = link
|
||||
s.mu.Unlock()
|
||||
return link, nil
|
||||
@@ -177,7 +160,7 @@ func (s *Storage) GetPermanent(path, source string, id uint) (*Link, error) {
|
||||
l, err := s.back.GetPermanent(path, source, id)
|
||||
if err == nil && l != nil {
|
||||
s.mu.Lock()
|
||||
s.setUsername(l)
|
||||
|
||||
s.shareCache[l.Hash] = l
|
||||
s.mu.Unlock()
|
||||
}
|
||||
@@ -194,9 +177,6 @@ func (s *Storage) Gets(sourcePath, source string, id uint) ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, l := range filtered {
|
||||
s.setUsername(l)
|
||||
}
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
@@ -222,9 +202,6 @@ func (s *Storage) GetBySourcePath(path, source string) ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, l := range filtered {
|
||||
s.setUsername(l)
|
||||
}
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ require (
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/time v0.11.0
|
||||
golang.org/x/time v0.12.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -109,7 +109,7 @@ require (
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695 // indirect
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
@@ -230,7 +230,7 @@ require (
|
||||
go-simpler.org/musttag v0.13.1 // indirect
|
||||
go-simpler.org/sloglint v0.11.0 // indirect
|
||||
go.augendre.info/fatcontext v0.8.0 // indirect
|
||||
go.etcd.io/bbolt v1.4.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
|
||||
+6
-6
@@ -285,8 +285,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695 h1:W3KSdht88NFfM71hd6Y4j2uiW81/aoqjPrMVoV/ph1E=
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac h1:gBtvd74wZT02fkOyoG651Sx2imv5lKdVvDwpUjKPFnw=
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -717,8 +717,8 @@ go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkye
|
||||
go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk=
|
||||
go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -947,8 +947,8 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
||||
@@ -27,6 +27,7 @@ type requestContext struct {
|
||||
fileInfo iteminfo.ExtendedFileInfo
|
||||
token string
|
||||
share *share.Link
|
||||
shareValid bool
|
||||
ctx context.Context
|
||||
MaxBandwidth int
|
||||
}
|
||||
@@ -50,6 +51,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
// Get the file link by hash
|
||||
link, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
data.share = &share.Link{}
|
||||
return http.StatusNotFound, fmt.Errorf("share hash not found")
|
||||
}
|
||||
if link.DisableAnonymous && data.user.Username == "anonymous" {
|
||||
@@ -77,18 +79,27 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
return http.StatusForbidden, fmt.Errorf("the target source is private")
|
||||
}
|
||||
// Get file information with options
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if link.DisableFileViewer {
|
||||
getContent = false
|
||||
}
|
||||
file, err := FileInfoFasterFunc(iteminfo.FileOptions{
|
||||
Path: utils.JoinPathAsUnix(link.Path, path),
|
||||
Source: link.Source,
|
||||
Modify: false,
|
||||
Expand: true,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
Content: getContent,
|
||||
})
|
||||
file.Token = link.Token
|
||||
if err != nil {
|
||||
logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err)
|
||||
return errToStatus(err), fmt.Errorf("error fetching share from server")
|
||||
}
|
||||
if getContent {
|
||||
link.Mu.Lock()
|
||||
link.Downloads++
|
||||
link.Mu.Unlock()
|
||||
}
|
||||
file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/")
|
||||
// Set the file info in the `data` object
|
||||
data.fileInfo = file
|
||||
@@ -114,9 +125,14 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||
func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
var link *share.Link
|
||||
var isShareRequest bool
|
||||
var shareHash string
|
||||
|
||||
hash := r.URL.Query().Get("hash")
|
||||
if hash != "" {
|
||||
// Get the file link by hash
|
||||
isShareRequest = true
|
||||
shareHash = hash
|
||||
link, _ = store.Share.GetByHash(hash)
|
||||
} else {
|
||||
prefix := config.Server.BaseURL + "public/share/"
|
||||
@@ -128,6 +144,8 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
remaining = remaining[:idx]
|
||||
}
|
||||
if remaining != "" {
|
||||
isShareRequest = true
|
||||
shareHash = remaining
|
||||
var err error
|
||||
link, err = store.Share.GetByHash(remaining)
|
||||
if err != nil {
|
||||
@@ -138,13 +156,24 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a share request, always create a share context (even if invalid)
|
||||
if isShareRequest {
|
||||
if link != nil {
|
||||
data.share = link
|
||||
data.shareValid = true
|
||||
} else {
|
||||
// Create an empty share with just the hash for invalid shares
|
||||
data.share = &share.Link{Hash: shareHash}
|
||||
data.shareValid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate user first
|
||||
status, err := withUserHelper(nil)(w, r, data)
|
||||
if err == nil && status < 400 {
|
||||
if link != nil {
|
||||
data.share = link
|
||||
if data.share != nil && data.shareValid {
|
||||
if data.user != nil {
|
||||
data.user.CustomTheme = link.ShareTheme
|
||||
data.user.CustomTheme = data.share.ShareTheme
|
||||
}
|
||||
}
|
||||
return fn(w, r, data)
|
||||
@@ -155,9 +184,8 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
// If user authentication failed, call the handler without user context
|
||||
// Clear any user data that might have been partially set
|
||||
data.token = ""
|
||||
if link != nil {
|
||||
data.share = link
|
||||
data.user.CustomTheme = link.ShareTheme
|
||||
if data.share != nil && data.shareValid {
|
||||
data.user.CustomTheme = data.share.ShareTheme
|
||||
}
|
||||
// Call the handler function without user context
|
||||
return fn(w, r, data)
|
||||
|
||||
+19
-2
@@ -45,8 +45,25 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
|
||||
fileList := []string{}
|
||||
for _, file := range strings.Split(f, "||") {
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
// Check if file already contains source prefix (source::path format)
|
||||
if strings.Contains(file, "::") {
|
||||
splitFile := strings.SplitN(file, "::", 2)
|
||||
if len(splitFile) == 2 {
|
||||
source := splitFile[0]
|
||||
path := splitFile[1]
|
||||
// Join the share path with the requested path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, path)
|
||||
fileList = append(fileList, source+"::"+filePath)
|
||||
} else {
|
||||
// Fallback: treat as plain path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
}
|
||||
} else {
|
||||
// Plain path without source prefix
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
}
|
||||
}
|
||||
|
||||
var status int
|
||||
|
||||
+16
-3
@@ -104,6 +104,7 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
}
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
@@ -173,15 +174,20 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
}
|
||||
return addSingleFile(filePath, relPath, zipWriter, tarWriter)
|
||||
})
|
||||
} else {
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
file, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
// If we get "is a directory" error, this is likely a symlink to a directory
|
||||
// that wasn't properly detected. Skip it gracefully.
|
||||
if strings.Contains(err.Error(), "is a directory") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -191,6 +197,11 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
return err
|
||||
}
|
||||
|
||||
// Double-check if this is actually a directory (in case of symlinks)
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
@@ -240,6 +251,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
}
|
||||
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
idx := indexing.GetIndex(firstFileSource)
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", firstFileSource)
|
||||
@@ -395,6 +407,7 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
|
||||
}
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
realPath, isDir, err := idx.GetRealPath(path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
@@ -85,6 +85,10 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if d.share != nil && d.share.DisableFileViewer {
|
||||
getContent = false
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
|
||||
Access: store.Access,
|
||||
Username: d.user.Username,
|
||||
@@ -92,7 +96,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
Content: getContent,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
|
||||
+82
-13
@@ -17,9 +17,35 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
)
|
||||
|
||||
// ShareResponse represents a share with computed username field and download URL
|
||||
type ShareResponse struct {
|
||||
*share.Link
|
||||
Username string `json:"username,omitempty"`
|
||||
DownloadURL string `json:"downloadURL,omitempty"`
|
||||
}
|
||||
|
||||
// convertToShareResponse converts shares to response format with usernames
|
||||
func convertToShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
|
||||
responses := make([]*ShareResponse, len(shares))
|
||||
for i, s := range shares {
|
||||
user, err := store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
responses[i] = &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
DownloadURL: getDownloadURL(r, s.Hash),
|
||||
}
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// shareListHandler returns a list of all share links.
|
||||
// @Summary List share links
|
||||
// @Description Returns a list of share links for the current user, or all links if the user is an admin.
|
||||
@@ -41,7 +67,11 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
shares = utils.NonNilSlice(shares)
|
||||
return renderJSON(w, r, shares)
|
||||
sharesWithUsernames, err := convertToShareResponse(r, shares)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, sharesWithUsernames)
|
||||
}
|
||||
|
||||
// shareGetsHandler retrieves share links for a specific resource path.
|
||||
@@ -74,16 +104,17 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
s, err := store.Share.Gets(scopePath, sourceInfo.Path, d.user.ID)
|
||||
if err == errors.ErrNotExist || len(s) == 0 {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
// Overwrite the Source field with the source name from the query for each link
|
||||
for _, link := range s {
|
||||
link.DownloadURL = getDownloadURL(r, link.Hash)
|
||||
return renderJSON(w, r, []*ShareResponse{})
|
||||
}
|
||||
// DownloadURL will be set in convertToShareResponse
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
sharesWithUsernames, err := convertToShareResponse(r, s)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, sharesWithUsernames)
|
||||
}
|
||||
|
||||
// shareDeleteHandler deletes a specific share link by its hash.
|
||||
@@ -190,11 +221,21 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
body.Path = s.Path
|
||||
body.Source = s.Source
|
||||
s.CommonShare = body.CommonShare
|
||||
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
// Convert to ShareResponse format with username
|
||||
var user *users.User
|
||||
user, err = store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
response := &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
source, ok := config.Server.NameToSource[body.Source]
|
||||
@@ -243,10 +284,21 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
CommonShare: body.CommonShare,
|
||||
}
|
||||
|
||||
if err := store.Share.Save(s); err != nil {
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
// Convert to ShareResponse format with username
|
||||
var user *users.User
|
||||
user, err = store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
response := &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
// DirectDownloadResponse represents the response for direct download endpoint
|
||||
@@ -410,9 +462,26 @@ func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *reque
|
||||
func getDownloadURL(r *http.Request, hash string) string {
|
||||
var downloadURL string
|
||||
if config.Server.ExternalUrl != "" {
|
||||
downloadURL = fmt.Sprintf("%spublic/api/raw?hash=%s", config.Server.ExternalUrl, hash)
|
||||
downloadURL = fmt.Sprintf("%s%spublic/api/raw?hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, hash)
|
||||
} else {
|
||||
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", getScheme(r), r.Host, config.Server.BaseURL, hash)
|
||||
// Prefer X-Forwarded-Host for proxy support
|
||||
var host string
|
||||
var scheme string
|
||||
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
|
||||
host = forwardedHost
|
||||
// Use X-Forwarded-Proto if available, otherwise default to https for proxied requests
|
||||
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
||||
scheme = forwardedProto
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple approach
|
||||
host = r.Host
|
||||
scheme = getScheme(r)
|
||||
}
|
||||
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", scheme, host, config.Server.BaseURL, hash)
|
||||
}
|
||||
return downloadURL
|
||||
}
|
||||
|
||||
+48
-38
@@ -45,6 +45,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
}
|
||||
|
||||
defaultThemeColor := "#455a64"
|
||||
staticURL := config.Server.BaseURL + "static"
|
||||
publicStaticURL := config.Server.BaseURL + "public/static"
|
||||
data := map[string]interface{}{
|
||||
@@ -56,50 +57,60 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"staticURL": staticURL,
|
||||
"baseURL": config.Server.BaseURL,
|
||||
"favicon": staticURL + "/img/icons/favicon-256x256.png",
|
||||
"color": "var(--primaryColor)",
|
||||
"color": defaultThemeColor,
|
||||
"winIcon": staticURL + "/img/icons/mstile-144x144.png",
|
||||
"appIcon": staticURL + "/img/icons/android-chrome-256x256.png",
|
||||
"description": "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server",
|
||||
}
|
||||
shareOverrides := map[string]interface{}{
|
||||
"isShare": false,
|
||||
"banner": "",
|
||||
"title": "",
|
||||
"quickDownload": false,
|
||||
"description": "",
|
||||
"themeColor": "",
|
||||
"disableThumbnails": false,
|
||||
"viewMode": "list",
|
||||
"disableFileViewer": false,
|
||||
"disableShareCard": false,
|
||||
"disableSidebar": false,
|
||||
shareProps := map[string]interface{}{
|
||||
"isShare": false,
|
||||
"isValid": false,
|
||||
"banner": "",
|
||||
"title": "",
|
||||
"quickDownload": false,
|
||||
"description": "",
|
||||
"themeColor": "",
|
||||
"disableThumbnails": false,
|
||||
"viewMode": "list",
|
||||
"disableFileViewer": false,
|
||||
"disableShareCard": false,
|
||||
"disableSidebar": false,
|
||||
"isPasswordProtected": false,
|
||||
"hash": "",
|
||||
}
|
||||
disableNavButtons := settings.Config.Frontend.DisableNavButtons
|
||||
if d.share != nil {
|
||||
disableNavButtons = disableNavButtons || d.share.HideNavButtons
|
||||
shareOverrides["viewMode"] = d.share.ViewMode
|
||||
shareOverrides["banner"] = d.share.Banner
|
||||
shareOverrides["title"] = d.share.Title
|
||||
shareOverrides["description"] = d.share.Description
|
||||
shareOverrides["themeColor"] = d.share.ThemeColor
|
||||
shareOverrides["quickDownload"] = d.share.QuickDownload
|
||||
shareOverrides["disableThumbnails"] = d.share.DisableThumbnails
|
||||
shareOverrides["disableFileViewer"] = d.share.DisablingFileViewer
|
||||
shareOverrides["disableShareCard"] = d.share.DisableShareCard
|
||||
shareOverrides["disableSidebar"] = d.share.DisableSidebar
|
||||
shareOverrides["isShare"] = true
|
||||
if d.share.Favicon != "" {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
data["favicon"] = d.share.Favicon
|
||||
} else {
|
||||
data["favicon"] = publicStaticURL + "/" + d.share.Favicon
|
||||
shareProps["isShare"] = true
|
||||
shareProps["isValid"] = d.shareValid
|
||||
shareProps["hash"] = d.share.Hash
|
||||
|
||||
if d.shareValid {
|
||||
disableNavButtons = disableNavButtons || d.share.HideNavButtons
|
||||
shareProps["viewMode"] = d.share.ViewMode
|
||||
shareProps["banner"] = d.share.Banner
|
||||
shareProps["title"] = d.share.Title
|
||||
shareProps["description"] = d.share.Description
|
||||
shareProps["themeColor"] = d.share.ThemeColor
|
||||
shareProps["quickDownload"] = d.share.QuickDownload
|
||||
shareProps["disableThumbnails"] = d.share.DisableThumbnails
|
||||
shareProps["disableFileViewer"] = d.share.DisableFileViewer
|
||||
shareProps["disableShareCard"] = d.share.DisableShareCard
|
||||
shareProps["disableSidebar"] = d.share.DisableSidebar
|
||||
shareProps["isPasswordProtected"] = d.share.PasswordHash != ""
|
||||
shareProps["downloadURL"] = getDownloadURL(r, d.share.Hash)
|
||||
if d.share.Favicon != "" {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
data["favicon"] = d.share.Favicon
|
||||
} else {
|
||||
data["favicon"] = publicStaticURL + "/" + d.share.Favicon
|
||||
}
|
||||
}
|
||||
if d.share.Description != "" {
|
||||
data["description"] = d.share.Description
|
||||
}
|
||||
if d.share.Title != "" {
|
||||
data["title"] = d.share.Title
|
||||
}
|
||||
}
|
||||
if d.share.Description != "" {
|
||||
data["description"] = d.share.Description
|
||||
}
|
||||
if d.share.Title != "" {
|
||||
data["title"] = d.share.Title
|
||||
}
|
||||
|
||||
// base url could be different for routes behind proxy
|
||||
@@ -107,7 +118,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png"
|
||||
data["winIcon"] = publicStaticURL + "/img/icons/mstile-144x144.png"
|
||||
data["appIcon"] = publicStaticURL + "/img/icons/android-chrome-256x256.png"
|
||||
fmt.Println("favicon", data["staticURL"])
|
||||
}
|
||||
// variables consumed by frontend as json
|
||||
data["globalVars"] = map[string]interface{}{
|
||||
@@ -132,7 +142,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"updateAvailable": utils.GetUpdateAvailableUrl(),
|
||||
"disableNavButtons": disableNavButtons,
|
||||
"userSelectableThemes": config.Frontend.Styling.CustomThemeOptions,
|
||||
"share": shareOverrides,
|
||||
"share": shareProps,
|
||||
}
|
||||
jsonVars, err := json.Marshal(data["globalVars"])
|
||||
if err != nil {
|
||||
|
||||
@@ -3260,9 +3260,6 @@ const docTemplate = `{
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3358,9 +3355,6 @@ const docTemplate = `{
|
||||
"userID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -3249,9 +3249,6 @@
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3347,9 +3344,6 @@
|
||||
"userID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -719,8 +719,6 @@ definitions:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
viewMode:
|
||||
type: string
|
||||
type: object
|
||||
@@ -788,8 +786,6 @@ definitions:
|
||||
type: string
|
||||
userID:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
viewMode:
|
||||
type: string
|
||||
type: object
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fetchURL, fetchJSON } from '@/api/utils'
|
||||
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
|
||||
import { notify } from '@/notify' // Import notify for error handling
|
||||
import { setNewToken } from '@/utils/auth.js' // Import setNewToken for token management
|
||||
import { shareOverrides } from '@/utils/constants'
|
||||
import { shareInfo } from '@/utils/constants'
|
||||
|
||||
export async function getAllUsers() {
|
||||
try {
|
||||
@@ -93,10 +93,9 @@ export async function login(username, password, recaptcha, otp) {
|
||||
export async function get(id) {
|
||||
try {
|
||||
let apiPath = getApiPath('api/users', { id: id })
|
||||
if (shareOverrides.isShare) {
|
||||
if (shareInfo.isShare) {
|
||||
apiPath = getPublicApiPath('users', { id: id })
|
||||
}
|
||||
console.log(apiPath,shareOverrides.isShare);
|
||||
return await fetchJSON(apiPath)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Failed to fetch user with ID: ${id}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { state } from "@/store";
|
||||
import { renew, logout } from "@/utils/auth";
|
||||
import { notify } from "@/notify";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export async function fetchURL(url, opts, auth = true) {
|
||||
opts = opts || {};
|
||||
@@ -34,7 +35,7 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
let error = new Error(await res.text());
|
||||
error.status = res.status;
|
||||
|
||||
if (auth && res.status == 401) {
|
||||
if (auth && res.status == 401 && !shareInfo.isShare) {
|
||||
logout();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isCardView" class="gallery-size card">
|
||||
<div v-if="showGallerySize" class="gallery-size card">
|
||||
{{ $t("files.size") }}<span class="sr-only">:</span> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
@@ -69,8 +69,8 @@ export default {
|
||||
addPadding() {
|
||||
return getters.isStickySidebar() || getters.isShare();
|
||||
},
|
||||
isCardView() {
|
||||
return getters.isCardView();
|
||||
showGallerySize() {
|
||||
return getters.isCardView() && state.req?.items?.length > 0;
|
||||
},
|
||||
items() {
|
||||
const req = state.req;
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount == 1 && isSearchActive"
|
||||
icon="folder"
|
||||
:label="$t('buttons.openParentFolder')"
|
||||
@action="openParentFolder"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="forward"
|
||||
@@ -117,6 +123,7 @@
|
||||
:class="{ 'dark-mode': isDarkMode }"
|
||||
>
|
||||
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('buttons.openFile')" @action="goToRaw()" />
|
||||
<action v-if="shouldShowParentFolder()" icon="folder" :label="$t('buttons.openParentFolder')" @action="openParentFolder" />
|
||||
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
|
||||
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
|
||||
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
|
||||
@@ -134,6 +141,7 @@ import buttons from "@/utils/buttons";
|
||||
import { notify } from "@/notify";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import { url } from "@/utils";
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
components: {
|
||||
@@ -258,8 +266,19 @@ export default {
|
||||
modify: state.user?.permissions?.modify,
|
||||
};
|
||||
},
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPrompt: {
|
||||
handler(prompt) {
|
||||
if (prompt && prompt.name === "ContextMenu") {
|
||||
this.setPositions();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
hasOverflowItems: {
|
||||
handler(hasItems) {
|
||||
mutations.setContextMenuHasItems(hasItems);
|
||||
@@ -287,6 +306,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shouldShowParentFolder() {
|
||||
return this.isPreview && state.req.path != "/";
|
||||
},
|
||||
showAccessHover() {
|
||||
mutations.showHover({
|
||||
name: "access",
|
||||
@@ -405,7 +427,8 @@ export default {
|
||||
},
|
||||
startDownload() {
|
||||
mutations.closeHovers();
|
||||
downloadFiles(state.selected);
|
||||
const items = state.selected.length > 0 ? state.selected : [state.req];
|
||||
downloadFiles(items);
|
||||
},
|
||||
goToRaw() {
|
||||
if (getters.isShare()) {
|
||||
@@ -447,6 +470,14 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
openParentFolder() {
|
||||
const item = state.selected.length > 0 ? state.selected[0] : state.req;
|
||||
let parentPath = url.removeLastDir(item.path);
|
||||
if (parentPath == "") {
|
||||
parentPath = "/";
|
||||
}
|
||||
url.goToItem(state.req.source, parentPath);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -306,13 +306,15 @@ export default {
|
||||
return this.showHelp;
|
||||
},
|
||||
activeStates() {
|
||||
// Create a Set of combined `name` and `type` keys for efficient lookup
|
||||
const selectedSet = new Set(
|
||||
state.selected.map((item) => `${item.name}:${item.type}`)
|
||||
);
|
||||
const result = this.results.map((s) => selectedSet.has(`${s.name}:${s.type}`));
|
||||
// Build a map of active states for the `results` array
|
||||
return result;
|
||||
const selectedItems = state.selected ? [].concat(state.selected) : [];
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
// Return an array of all false if nothing is selected
|
||||
return new Array(this.results.length).fill(false);
|
||||
}
|
||||
|
||||
const selectedPaths = new Set(selectedItems.map((item) => item.path));
|
||||
return this.results.map((result) => selectedPaths.has(result.path));
|
||||
},
|
||||
sourceInfo() {
|
||||
return state.sources.info;
|
||||
@@ -519,17 +521,13 @@ export default {
|
||||
},
|
||||
addSelected(event, s) {
|
||||
const pathParts = url.removeTrailingSlash(s.path).split("/");
|
||||
let path = this.getContext + s.path;
|
||||
if (this.getContext === "/") {
|
||||
path = s.path;
|
||||
}
|
||||
let path = this.getContext + url.removeTrailingSlash(s.path);
|
||||
const modifiedItem = {
|
||||
name: pathParts.pop(),
|
||||
path: path,
|
||||
size: s.size,
|
||||
type: s.type,
|
||||
source: this.selectedSource || state.sources.current,
|
||||
fullPath: path,
|
||||
};
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(modifiedItem);
|
||||
@@ -566,6 +564,16 @@ export default {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.searchContext.input option {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.searchContext.input option:hover {
|
||||
background: var(--primaryColor);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#results > #result-list {
|
||||
max-height: 80vh;
|
||||
width: 35em;
|
||||
@@ -671,9 +679,8 @@ export default {
|
||||
/* IE and Edge */
|
||||
}
|
||||
|
||||
.search-entry {
|
||||
cursor: pointer;
|
||||
border-radius: 0.25em;
|
||||
.search-entry:hover {
|
||||
background-color: var(--alt-background);
|
||||
}
|
||||
|
||||
.search-entry.active {
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from "@/utils/constants";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { mutations, state } from "@/store";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
// NEW: Define placeholder and error image URLs for easy configuration
|
||||
const PLACEHOLDER_URL = baseURL + "static/img/placeholder.png"; // A generic loading placeholder
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
hasPreviewImage() {
|
||||
if (shareOverrides.disableThumbnails) {
|
||||
if (shareInfo.disableThumbnails) {
|
||||
return false;
|
||||
}
|
||||
return this.isPreviewImg && this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled
|
||||
|
||||
@@ -75,7 +75,7 @@ import * as upload from "@/utils/upload";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { url } from "@/utils";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
import { baseURL, serverHasMultipleSources, shareOverrides } from "@/utils/constants";
|
||||
import { baseURL, serverHasMultipleSources, shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
// @ts-ignore
|
||||
if (getters.isShare()) {
|
||||
// @ts-ignore
|
||||
return shareOverrides.quickDownload && !this.isDir;
|
||||
return shareInfo.quickDownload && !this.isDir;
|
||||
}
|
||||
// @ts-ignore
|
||||
return state.user?.quickDownload && !this.galleryView && !this.isDir;
|
||||
|
||||
+12
-12
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="share card">
|
||||
<div class="share__box">
|
||||
<div v-if="shareOverrides.banner" class="banner">
|
||||
<div v-if="shareInfo.banner" class="banner">
|
||||
<img :src="getShareBanner" />
|
||||
</div>
|
||||
<div v-if="shareOverrides.title" class="share__box__element">
|
||||
<h3>{{ shareOverrides.title }}</h3>
|
||||
<div v-if="shareInfo.title" class="share__box__element">
|
||||
<h3>{{ shareInfo.title }}</h3>
|
||||
</div>
|
||||
<div v-if="shareOverrides.description" class="share__box__element">
|
||||
<p>{{ shareOverrides.description }}</p>
|
||||
<div v-if="shareInfo.description" class="share__box__element">
|
||||
<p>{{ shareInfo.description }}</p>
|
||||
</div>
|
||||
<hr v-if="shareOverrides.banner || shareOverrides.title || shareOverrides.description" />
|
||||
<hr v-if="shareInfo.banner || shareInfo.title || shareInfo.description" />
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@ import { state, getters } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "ShareInfo",
|
||||
@@ -61,13 +61,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
getShareBanner() {
|
||||
if (this.shareOverrides.banner.startsWith("http")) {
|
||||
return this.shareOverrides.banner;
|
||||
if (this.shareInfo.banner.startsWith("http")) {
|
||||
return this.shareInfo.banner;
|
||||
}
|
||||
return publicApi.getDownloadURL(state.share, [this.shareOverrides.banner]);
|
||||
return publicApi.getDownloadURL(state.share, [this.shareInfo.banner]);
|
||||
},
|
||||
shareOverrides() {
|
||||
return shareOverrides;
|
||||
shareInfo() {
|
||||
return shareInfo;
|
||||
},
|
||||
req() {
|
||||
return state.req;
|
||||
@@ -80,7 +80,7 @@
|
||||
<ToggleSwitch class="item" v-model="allowEdit" :name="'Allow modifications'" />
|
||||
<ToggleSwitch class="item" v-model="allowUpload" :name="'Allow uploading'" />
|
||||
-->
|
||||
<ToggleSwitch class="item" v-model="disablingFileViewer" :name="'Disable File Viewer'" />
|
||||
<ToggleSwitch class="item" v-model="disableFileViewer" :name="'Disable File Viewer'" />
|
||||
<ToggleSwitch
|
||||
class="item"
|
||||
v-model="quickDownload"
|
||||
@@ -253,9 +253,10 @@ import { state, getters, mutations } from "@/store";
|
||||
import { publicApi } from "@/api";
|
||||
import Clipboard from "clipboard";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import { buildItemUrl } from "@/utils/url";
|
||||
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
|
||||
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
|
||||
import { userSelectableThemes } from "@/utils/constants";
|
||||
import { userSelectableThemes, externalUrl } from "@/utils/constants";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
//import ViewMode from "@/components/settings/ViewMode.vue";
|
||||
|
||||
export default {
|
||||
@@ -299,7 +300,7 @@ export default {
|
||||
disableAnonymous: false,
|
||||
allowUpload: false,
|
||||
maxBandwidth: "",
|
||||
disablingFileViewer: false,
|
||||
disableFileViewer: false,
|
||||
disableThumbnails: false,
|
||||
enableAllowedUsernames: false,
|
||||
allowedUsernames: "",
|
||||
@@ -376,6 +377,7 @@ export default {
|
||||
this.shareTheme = this.link.shareTheme || "default";
|
||||
this.disableAnonymous = this.link.disableAnonymous || false;
|
||||
this.disableThumbnails = this.link.disableThumbnails || false;
|
||||
this.disableFileViewer = this.link.disableFileViewer || false;
|
||||
this.enableAllowedUsernames = Array.isArray(this.link.allowedUsernames) && this.link.allowedUsernames.length > 0;
|
||||
this.allowedUsernames = this.enableAllowedUsernames ? this.link.allowedUsernames.join(", ") : "";
|
||||
this.keepAfterExpiration = this.link.keepAfterExpiration || false;
|
||||
@@ -491,7 +493,7 @@ export default {
|
||||
maxBandwidth: this.maxBandwidth ? parseInt(this.maxBandwidth) : 0,
|
||||
downloadsLimit: this.downloadsLimit ? parseInt(this.downloadsLimit) : 0,
|
||||
shareTheme: this.shareTheme,
|
||||
disablingFileViewer: this.disablingFileViewer,
|
||||
disableFileViewer: this.disableFileViewer,
|
||||
disableThumbnails: this.disableThumbnails,
|
||||
allowedUsernames: this.enableAllowedUsernames ? this.allowedUsernames.split(',').map(u => u.trim()) : [],
|
||||
keepAfterExpiration: this.keepAfterExpiration,
|
||||
@@ -516,8 +518,9 @@ export default {
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
} else {
|
||||
// reload page to see changes
|
||||
window.location.reload();
|
||||
// emit event to reload shares in settings view
|
||||
eventBus.emit('sharesChanged');
|
||||
mutations.closeHovers();
|
||||
}
|
||||
|
||||
this.time = "";
|
||||
@@ -538,6 +541,8 @@ export default {
|
||||
try {
|
||||
await publicApi.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
// emit event to reload shares in settings view
|
||||
eventBus.emit('sharesChanged');
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
@@ -565,8 +570,8 @@ export default {
|
||||
* @param {Share} share
|
||||
*/
|
||||
buildDownloadLink(share) {
|
||||
if (share.downloadURL) {
|
||||
return share.downloadURL;
|
||||
if (share.downloadURL && externalUrl == "") {
|
||||
return this.fixDownloadURL(share.downloadURL);
|
||||
}
|
||||
return publicApi.getDownloadURL(share, [this.item.name]);
|
||||
},
|
||||
@@ -585,6 +590,9 @@ export default {
|
||||
|
||||
this.listing = !this.listing;
|
||||
},
|
||||
fixDownloadURL(downloadUrl) {
|
||||
return fixDownloadURL(downloadUrl);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
@click="toggleClick"
|
||||
@mouseenter="showTooltip($event, $t('index.toggleClick'))"
|
||||
@mouseleave="hideTooltip"
|
||||
v-if="!isInvalidShare"
|
||||
>
|
||||
<i class="material-icons">ads_click</i>
|
||||
</div>
|
||||
@@ -139,7 +140,7 @@
|
||||
|
||||
<script>
|
||||
import * as auth from "@/utils/auth";
|
||||
import { signup, disableExternal, noAuth, loginPage } from "@/utils/constants";
|
||||
import { signup, disableExternal, noAuth, loginPage, shareInfo } from "@/utils/constants";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
|
||||
@@ -156,11 +157,12 @@ export default {
|
||||
computed: {
|
||||
disableQuickToggles: () => state.user.disableQuickToggles,
|
||||
hasSourceInfo: () => state.sources.hasSourceInfo,
|
||||
hideSidebarFileActions: () => state.user.hideSidebarFileActions,
|
||||
hideSidebarFileActions: () => state.user.hideSidebarFileActions || getters.isInvalidShare(),
|
||||
settingsAllowed: () => !state.user.disableSettings,
|
||||
isSettings: () => getters.isSettings(),
|
||||
isStickySidebar: () => getters.isStickySidebar(),
|
||||
isMobile: () => getters.isMobile(),
|
||||
isInvalidShare: () => getters.isInvalidShare(),
|
||||
isListingView: () => getters.currentView() == "listingView",
|
||||
user: () => (state.user),
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="share-sidebar">
|
||||
<div class="share-sidebar__content">
|
||||
<ShareInfo
|
||||
<ShareInfoCard
|
||||
v-if="!disableShareCard"
|
||||
:hash="hash"
|
||||
:token="token"
|
||||
@@ -12,13 +12,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ShareInfo from "@/components/files/ShareInfo.vue";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import ShareInfoCard from "@/components/files/ShareInfoCard.vue";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "SidebarShare",
|
||||
components: {
|
||||
ShareInfo,
|
||||
ShareInfoCard,
|
||||
},
|
||||
props: {
|
||||
hash: {
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
disableShareCard() {
|
||||
return shareOverrides.disableShareCard;
|
||||
return shareInfo.disableShareCard;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<SidebarSettings v-if="isSettings"></SidebarSettings>
|
||||
<SidebarGeneral v-if="!isSettings"></SidebarGeneral>
|
||||
<SidebarShare v-if="isShare"></SidebarShare>
|
||||
<SidebarShare v-if="isValidShare"></SidebarShare>
|
||||
|
||||
<div class="buffer"></div>
|
||||
<div v-if="!isSettings" class="credits">
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
mutations.setSeenUpdate(localStorage.getItem("seenUpdate"));
|
||||
},
|
||||
computed: {
|
||||
isShare: () => getters.isShare(),
|
||||
isValidShare: () => getters.isValidShare(),
|
||||
releaseUrl: () => updateAvailable,
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
|
||||
@@ -224,7 +224,7 @@ body.rtl .card-title>*:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card .card-content.full {
|
||||
.card-content.full {
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ body.rtl #listingView {
|
||||
}
|
||||
|
||||
#listingView.compact .item:hover,
|
||||
#listingView.list .item:hover {
|
||||
#listingView.list .item:hover,
|
||||
.search-entry:hover {
|
||||
width: 98% !important;
|
||||
margin-left: 1% !important;
|
||||
padding-left: 0.5em !important;
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "استئناف الكل",
|
||||
"clearCompleted": "اكتمل المسح",
|
||||
"showMore": "اعرض المزيد",
|
||||
"showLess": "اعرض أقل"
|
||||
"showLess": "اعرض أقل",
|
||||
"openParentFolder": "فتح المجلد الأصل"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "لقد حدث خطأ ما.",
|
||||
"notFound": "لا يمكن الوصول لهذا المحتوى.",
|
||||
"connection": "لا يمكن الوصول إلى الخادم"
|
||||
"connection": "لا يمكن الوصول إلى الخادم",
|
||||
"shareNotFound": "حصة غير صالحة أو منتهية الصلاحية."
|
||||
},
|
||||
"files": {
|
||||
"body": "الصفحة",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "إدارة الوصول",
|
||||
"totalDenied": "مرفوض",
|
||||
"totalAllowed": "المسموح به",
|
||||
"all": "رفض الكل"
|
||||
"all": "رفض الكل",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "التحميلات والتنزيلات",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Obnovit vše",
|
||||
"clearCompleted": "Vymazání dokončeno",
|
||||
"showMore": "Zobrazit více",
|
||||
"showLess": "Zobrazit méně"
|
||||
"showLess": "Zobrazit méně",
|
||||
"openParentFolder": "Otevření nadřazené složky"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stáhnout soubor",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "Nemáte oprávnění k přístupu.",
|
||||
"internal": "Něco se opravdu pokazilo.",
|
||||
"notFound": "Tuto položku nelze najít.",
|
||||
"connection": "Nelze se připojit k serveru."
|
||||
"connection": "Nelze se připojit k serveru.",
|
||||
"shareNotFound": "Neplatná nebo prošlá akcie."
|
||||
},
|
||||
"files": {
|
||||
"body": "Tělo",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "Přidáno pravidlo přístupu!",
|
||||
"name": "Název",
|
||||
"accessManagement": "Správa přístupu",
|
||||
"all": "Odmítnout vše"
|
||||
"all": "Odmítnout vše",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nepodařilo se inicializovat editor. Prosím, načtěte jej znovu.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Alle zusammenfassen",
|
||||
"clearCompleted": "Abgeschlossene löschen",
|
||||
"showMore": "Mehr anzeigen",
|
||||
"showLess": "Weniger anzeigen"
|
||||
"showLess": "Weniger anzeigen",
|
||||
"openParentFolder": "Übergeordneten Ordner öffnen"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Datei herunterladen",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Sie haben keine Berechtigung dies abzurufen.",
|
||||
"internal": "Etwas ist schief gelaufen.",
|
||||
"notFound": "Dieser Ort kann nicht angezeigt werden.",
|
||||
"connection": "Der Server ist nicht erreichbar."
|
||||
"connection": "Der Server ist nicht erreichbar.",
|
||||
"shareNotFound": "Ungültige oder abgelaufene Aktie."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Zugangsverwaltung",
|
||||
"totalDenied": "Verweigert",
|
||||
"totalAllowed": "Erlaubt",
|
||||
"all": "Alle ablehnen"
|
||||
"all": "Alle ablehnen",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads & Downloads",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Συνέχισε όλα",
|
||||
"clearCompleted": "Εκκαθάριση ολοκληρώθηκε",
|
||||
"showMore": "Εμφάνιση περισσότερων",
|
||||
"showLess": "Εμφάνιση λιγότερων"
|
||||
"showLess": "Εμφάνιση λιγότερων",
|
||||
"openParentFolder": "Άνοιγμα γονικού φακέλου"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Λήψη αρχείου",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
|
||||
"internal": "Προέκυψε εσωτερικό σφάλμα.",
|
||||
"notFound": "Αυτή η τοποθεσία δεν μπορεί να βρεθεί.",
|
||||
"connection": "Ο διακομιστής δεν είναι διαθέσιμος."
|
||||
"connection": "Ο διακομιστής δεν είναι διαθέσιμος.",
|
||||
"shareNotFound": "Άκυρη ή ληγμένη μετοχή."
|
||||
},
|
||||
"files": {
|
||||
"body": "Περιεχόμενο",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Διαχείριση πρόσβασης",
|
||||
"totalDenied": "Απορρίπτεται",
|
||||
"totalAllowed": "Επιτρεπόμενο",
|
||||
"all": "Άρνηση όλων"
|
||||
"all": "Άρνηση όλων",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Ανεβάσματα & λήψεις",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Resume all",
|
||||
"clearCompleted": "Clear completed",
|
||||
"showMore": "Show more",
|
||||
"showLess": "Show less"
|
||||
"showLess": "Show less",
|
||||
"openParentFolder": "Open parent folder"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "Something really went wrong.",
|
||||
"notFound": "This location can't be reached.",
|
||||
"connection": "The server can't be reached."
|
||||
"connection": "The server can't be reached.",
|
||||
"shareNotFound": "Invalid or expired share."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Reanudar todo",
|
||||
"clearCompleted": "Limpiar lista",
|
||||
"showMore": "Ver más",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir carpeta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descargar archivo",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "No tienes permisos para acceder.",
|
||||
"internal": "Error interno :(",
|
||||
"notFound": "No se encontró el directorio",
|
||||
"connection": "No se puede conectar al servidor."
|
||||
"connection": "No se puede conectar al servidor.",
|
||||
"shareNotFound": "Acción no válida o caducada."
|
||||
},
|
||||
"files": {
|
||||
"body": "Contenido",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Gestión de accesos",
|
||||
"totalDenied": "Denegado",
|
||||
"totalAllowed": "Permitido",
|
||||
"all": "Denegar todo"
|
||||
"all": "Denegar todo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Carga de archivos",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Curriculum vitae",
|
||||
"clearCompleted": "Effacement terminé",
|
||||
"showMore": "Afficher plus",
|
||||
"showLess": "Montrer moins"
|
||||
"showLess": "Montrer moins",
|
||||
"openParentFolder": "Ouvrir le dossier parent"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Télécharger le fichier",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Vous n'avez pas la permission d'accéder à cela.",
|
||||
"internal": "Aïe ! Quelque chose s'est mal passé.",
|
||||
"notFound": "Impossible d'accéder à cet emplacement.",
|
||||
"connection": "Le serveur n'est pas accessible."
|
||||
"connection": "Le serveur n'est pas accessible.",
|
||||
"shareNotFound": "Action non valide ou expirée."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corps",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Gestion de l'accès",
|
||||
"totalDenied": "Refusé",
|
||||
"totalAllowed": "Autorisé",
|
||||
"all": "Refuser tout"
|
||||
"all": "Refuser tout",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Chargements et téléchargements",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "המשך הכל",
|
||||
"clearCompleted": "השלם",
|
||||
"showMore": "הצג עוד",
|
||||
"showLess": "הצג פחות"
|
||||
"showLess": "הצג פחות",
|
||||
"openParentFolder": "פתח את התיקייה הראשית"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "אין לך הרשאות גישה",
|
||||
"internal": "משהו השתבש",
|
||||
"notFound": "לא ניתן להגיע למיקום זה",
|
||||
"connection": "לא ניתן להגיע לשרת"
|
||||
"connection": "לא ניתן להגיע לשרת",
|
||||
"shareNotFound": "שיתוף לא חוקי או פג תוקף."
|
||||
},
|
||||
"files": {
|
||||
"body": "גוף",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "ניהול גישה",
|
||||
"totalDenied": "נדחה",
|
||||
"totalAllowed": "מותר",
|
||||
"all": "דחה הכל"
|
||||
"all": "דחה הכל",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "העלאות והורדות",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Folytassa az összes",
|
||||
"clearCompleted": "Tiszta befejezve",
|
||||
"showMore": "Többet mutasson",
|
||||
"showLess": "Kevesebb mutatása"
|
||||
"showLess": "Kevesebb mutatása",
|
||||
"openParentFolder": "Szülői mappa megnyitása"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Fájl letöltése",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nincs jogosultsága a hozzáféréshez.",
|
||||
"internal": "Valami nagyon elromlott.",
|
||||
"notFound": "Ez a hely nem érhető el.",
|
||||
"connection": "A kiszolgáló nem érhető el."
|
||||
"connection": "A kiszolgáló nem érhető el.",
|
||||
"shareNotFound": "Érvénytelen vagy lejárt részvény."
|
||||
},
|
||||
"files": {
|
||||
"body": "Törzs",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Hozzáférés-kezelés",
|
||||
"totalDenied": "Elutasítva",
|
||||
"totalAllowed": "Engedélyezett",
|
||||
"all": "Mindent megtagadni"
|
||||
"all": "Mindent megtagadni",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Feltöltések és letöltések",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Riprendete tutti",
|
||||
"clearCompleted": "Completato il lavoro di pulizia",
|
||||
"showMore": "Mostra di più",
|
||||
"showLess": "Mostra meno"
|
||||
"showLess": "Mostra meno",
|
||||
"openParentFolder": "Aprire la cartella madre"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Scarica file",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Non hai i permessi per accedere a questo file.",
|
||||
"internal": "Qualcosa è andato veramente male.",
|
||||
"notFound": "Questo percorso non può essere raggiunto.",
|
||||
"connection": "Il server non è raggiungibile."
|
||||
"connection": "Il server non è raggiungibile.",
|
||||
"shareNotFound": "Azione non valida o scaduta."
|
||||
},
|
||||
"files": {
|
||||
"body": "Contenuto",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Gestione degli accessi",
|
||||
"totalDenied": "Negato",
|
||||
"totalAllowed": "Consentito",
|
||||
"all": "Rifiuta tutto"
|
||||
"all": "Rifiuta tutto",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Caricamenti e download",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "すべての履歴書",
|
||||
"clearCompleted": "クリア完了",
|
||||
"showMore": "もっと見る",
|
||||
"showLess": "もっと見る"
|
||||
"showLess": "もっと見る",
|
||||
"openParentFolder": "親フォルダを開く"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "内部エラーが発生しました。",
|
||||
"notFound": "リソースが見つからなりませんでした。",
|
||||
"connection": "サーバーにアクセスできません。"
|
||||
"connection": "サーバーにアクセスできません。",
|
||||
"shareNotFound": "無効または期限切れの共有。"
|
||||
},
|
||||
"files": {
|
||||
"body": "本文",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "アクセス管理",
|
||||
"totalDenied": "却下",
|
||||
"totalAllowed": "可",
|
||||
"all": "すべて拒否"
|
||||
"all": "すべて拒否",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。"
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "アップロードとダウンロード",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "모두 다시 시작",
|
||||
"clearCompleted": "완료 지우기",
|
||||
"showMore": "자세히 보기",
|
||||
"showLess": "덜 보기"
|
||||
"showLess": "덜 보기",
|
||||
"openParentFolder": "상위 폴더 열기"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "파일 다운로드",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "접근 권한이 없습니다.",
|
||||
"internal": "오류가 발생하였습니다.",
|
||||
"notFound": "해당 경로를 찾을 수 없습니다.",
|
||||
"connection": "서버에 연결할 수 없습니다."
|
||||
"connection": "서버에 연결할 수 없습니다.",
|
||||
"shareNotFound": "유효하지 않거나 만료된 공유입니다."
|
||||
},
|
||||
"files": {
|
||||
"body": "본문",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "액세스 관리",
|
||||
"totalDenied": "거부됨",
|
||||
"totalAllowed": "허용됨",
|
||||
"all": "모두 거부"
|
||||
"all": "모두 거부",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "업로드 및 다운로드",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Alles hervatten",
|
||||
"clearCompleted": "Gereed",
|
||||
"showMore": "Meer tonen",
|
||||
"showLess": "Minder tonen"
|
||||
"showLess": "Minder tonen",
|
||||
"openParentFolder": "Bovenliggende map openen"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "U hebt geen rechten om hier toegang toe te krijgen.",
|
||||
"internal": "Er ging iets mis.",
|
||||
"notFound": "Deze locatie kan niet worden bereikt.",
|
||||
"connection": "De server kan niet worden bereikt."
|
||||
"connection": "De server kan niet worden bereikt.",
|
||||
"shareNotFound": "Ongeldig of verlopen aandeel."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "Toegangsregel toegevoegd!",
|
||||
"name": "Naam",
|
||||
"accessManagement": "Toegangsbeheer",
|
||||
"all": "Alles weigeren"
|
||||
"all": "Alles weigeren",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Het is niet gelukt de editor te initialiseren. Gelieve opnieuw te laden.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Resume all",
|
||||
"clearCompleted": "Czyszczenie zakończone",
|
||||
"showMore": "Pokaż więcej",
|
||||
"showLess": "Pokaż mniej"
|
||||
"showLess": "Pokaż mniej",
|
||||
"openParentFolder": "Otwórz folder nadrzędny"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Pobierz plik",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nie posiadasz uprawnień potrzebnych, by uzyskać do tego dostęp.",
|
||||
"internal": "Pojawił się poważny problem.",
|
||||
"notFound": "Ten adres nie jest poprawny.",
|
||||
"connection": "Nie można uzyskać dostępu do serwera."
|
||||
"connection": "Nie można uzyskać dostępu do serwera.",
|
||||
"shareNotFound": "Nieprawidłowy lub wygasły udział."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Zarządzanie dostępem",
|
||||
"totalDenied": "Odmowa",
|
||||
"totalAllowed": "Dozwolone",
|
||||
"all": "Odmów wszystkim"
|
||||
"all": "Odmów wszystkim",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Przesyłanie i pobieranie",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Retomar tudo",
|
||||
"clearCompleted": "Limpar concluído",
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Baixar arquivo",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Você não tem permissões para acessar isto.",
|
||||
"internal": "Ops! Algum erro ocorreu.",
|
||||
"notFound": "Ops! Nada foi encontrado.",
|
||||
"connection": "O servidor não pode ser alcançado."
|
||||
"connection": "O servidor não pode ser alcançado.",
|
||||
"shareNotFound": "Compartilhamento inválido ou expirado."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corpo",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Gerenciamento de acesso",
|
||||
"totalDenied": "Negado",
|
||||
"totalAllowed": "Permitido",
|
||||
"all": "Negar tudo"
|
||||
"all": "Negar tudo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads e downloads",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Retomar tudo",
|
||||
"clearCompleted": "Limpar concluído",
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarregar ficheiro",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Não tem permissões para aceder a isto",
|
||||
"internal": "Algo correu bastante mal.",
|
||||
"notFound": "Esta localização não é alcançável.",
|
||||
"connection": "O servidor não pode ser contactado."
|
||||
"connection": "O servidor não pode ser contactado.",
|
||||
"shareNotFound": "Ação inválida ou expirada."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corpo",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "Regra de acesso adicionada!",
|
||||
"name": "Nome",
|
||||
"accessManagement": "Gestão do acesso",
|
||||
"all": "Recusar tudo"
|
||||
"all": "Recusar tudo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Falha ao inicializar o editor. Por favor, recarregue.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "CV toate",
|
||||
"clearCompleted": "Golire finalizată",
|
||||
"showMore": "Arată mai multe",
|
||||
"showLess": "Arată mai puțin"
|
||||
"showLess": "Arată mai puțin",
|
||||
"openParentFolder": "Deschideți folderul părinte"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarcă fișier",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nu ai permisiuni sa accesezi asta.",
|
||||
"internal": "Ceva nu a funcționat corect.",
|
||||
"notFound": "Aceasta locație nu poate fi accesată.",
|
||||
"connection": "Serverul nu poate fi accesat."
|
||||
"connection": "Serverul nu poate fi accesat.",
|
||||
"shareNotFound": "Acțiune invalidă sau expirată."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corp",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Gestionarea accesului",
|
||||
"totalDenied": "Refuzat",
|
||||
"totalAllowed": "Permise",
|
||||
"all": "Refuzați toate"
|
||||
"all": "Refuzați toate",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Încărcări și descărcări",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Резюме все",
|
||||
"clearCompleted": "Очистка завершена",
|
||||
"showMore": "Показать еще",
|
||||
"showLess": "Показать меньше"
|
||||
"showLess": "Показать меньше",
|
||||
"openParentFolder": "Откройте родительскую папку"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Скачать файл",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "У вас нет прав доступа к этому.",
|
||||
"internal": "Что-то пошло не так.",
|
||||
"notFound": "Неправильная ссылка.",
|
||||
"connection": "Нет подключения к серверу."
|
||||
"connection": "Нет подключения к серверу.",
|
||||
"shareNotFound": "Недействительный или истекший ресурс."
|
||||
},
|
||||
"files": {
|
||||
"body": "Тело",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Управление доступом",
|
||||
"totalDenied": "Отказано",
|
||||
"totalAllowed": "Разрешено",
|
||||
"all": "Запретить все"
|
||||
"all": "Запретить все",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Загрузка и скачивание",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Obnoviť všetko",
|
||||
"clearCompleted": "Vyčistiť dokončené",
|
||||
"showMore": "Zobraziť viac",
|
||||
"showLess": "Zobraziť menej"
|
||||
"showLess": "Zobraziť menej",
|
||||
"openParentFolder": "Otvorenie nadradeného priečinka"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stiahnuť súbor",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "Something really went wrong.",
|
||||
"notFound": "This location can't be reached.",
|
||||
"connection": "The server can't be reached."
|
||||
"connection": "The server can't be reached.",
|
||||
"shareNotFound": "Neplatná alebo neplatná akcia."
|
||||
},
|
||||
"files": {
|
||||
"body": "Telo",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Správa prístupu",
|
||||
"totalDenied": "Odmietnuté",
|
||||
"totalAllowed": "Povolené",
|
||||
"all": "Odmietnuť všetko"
|
||||
"all": "Odmietnuť všetko",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Toto ukazuje, čo sa stane, keď sa používateľ pokúsi získať prístup k ceste, kde neexistujú žiadne špecifické pravidlá prístupu. \"Allow\" (Povoliť) znamená, že používatelia majú predvolený prístup, \"Deny\" (Odmietnuť) znamená, že používatelia sú predvolene blokovaní, pokiaľ to nie je výslovne povolené."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Nahrávanie a sťahovanie",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Återuppta alla",
|
||||
"clearCompleted": "Klart avslutat",
|
||||
"showMore": "Visa mer",
|
||||
"showLess": "Visa mindre"
|
||||
"showLess": "Visa mindre",
|
||||
"openParentFolder": "Öppna överordnad mapp"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Ladda ner fil",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Du saknar rättigheter till detta",
|
||||
"internal": "Något gick fel",
|
||||
"notFound": "Det går inte att nå den här platsen.",
|
||||
"connection": "Det går inte att nå servern."
|
||||
"connection": "Det går inte att nå servern.",
|
||||
"shareNotFound": "Ogiltig eller utgången andel."
|
||||
},
|
||||
"files": {
|
||||
"body": "Huvud",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "Tillträdesregel tillagd!",
|
||||
"name": "Namn",
|
||||
"accessManagement": "Hantering av åtkomst",
|
||||
"all": "Avvisa alla"
|
||||
"all": "Avvisa alla",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Detta visar vad som händer när en användare försöker komma åt en sökväg där det inte finns några specifika åtkomstregler. \"Allow\" innebär att användare kan komma åt som standard, \"Deny\" innebär att användare blockeras som standard om de inte uttryckligen tillåts."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Redigeraren kunde inte initieras. Vänligen ladda om.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Tümünü devam ettir",
|
||||
"clearCompleted": "Temizleme tamamlandı",
|
||||
"showMore": "Daha fazla göster",
|
||||
"showLess": "Daha az göster"
|
||||
"showLess": "Daha az göster",
|
||||
"openParentFolder": "Üst klasörü açın"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Dosyayı indir",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Buna erişim izniniz yok.",
|
||||
"internal": "Bir şeyler ters gitti.",
|
||||
"notFound": "Bu konuma ulaşılamıyor.",
|
||||
"connection": "Sunucuya ulaşılamıyor."
|
||||
"connection": "Sunucuya ulaşılamıyor.",
|
||||
"shareNotFound": "Geçersiz veya süresi dolmuş paylaşım."
|
||||
},
|
||||
"files": {
|
||||
"body": "Sayfa",
|
||||
@@ -444,7 +446,9 @@
|
||||
"accessManagement": "Erişim Yönetimi",
|
||||
"totalDenied": "Reddedildi",
|
||||
"totalAllowed": "İzin verildi",
|
||||
"all": "Tümünü Reddet"
|
||||
"all": "Tümünü Reddet",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Bu, bir kullanıcı belirli erişim kurallarının bulunmadığı bir yola erişmeye çalıştığında ne olacağını gösterir. 'Allow' kullanıcıların varsayılan olarak erişebileceği anlamına gelir, 'Deny' ise açıkça izin verilmediği sürece kullanıcıların varsayılan olarak engellendiği anlamına gelir."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Yüklemeler & İndirmeler",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Відновити все",
|
||||
"clearCompleted": "Очищення завершено",
|
||||
"showMore": "Показати більше",
|
||||
"showLess": "Показати менше"
|
||||
"showLess": "Показати менше",
|
||||
"openParentFolder": "Відкрийте батьківську папку"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Завантажити файл",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "У вас немає прав доступу до цього.",
|
||||
"internal": "Щось пішло не так.",
|
||||
"notFound": "Неправильне посилання.",
|
||||
"connection": "Немає підключення до сервера."
|
||||
"connection": "Немає підключення до сервера.",
|
||||
"shareNotFound": "Недійсна або прострочена акція."
|
||||
},
|
||||
"files": {
|
||||
"body": "Тіло",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "Додано правило доступу!",
|
||||
"name": "Ім'я",
|
||||
"accessManagement": "Керування доступом",
|
||||
"all": "Заперечувати все."
|
||||
"all": "Заперечувати все.",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Тут показано, що відбувається, коли користувач намагається отримати доступ до шляху, для якого не існує певних правил доступу. \"Дозволити\" означає, що користувачі можуть отримати доступ за замовчуванням, \"Заборонити\" означає, що користувачі за замовчуванням заблоковані, якщо тільки це не дозволено явно."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Не вдалося ініціалізувати редактор. Будь ласка, перезавантажте.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "全部简历",
|
||||
"clearCompleted": "清除完毕",
|
||||
"showMore": "显示更多",
|
||||
"showLess": "显示更少"
|
||||
"showLess": "显示更少",
|
||||
"openParentFolder": "打开父文件夹"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "你无权限访问",
|
||||
"internal": "服务器出了点问题。",
|
||||
"notFound": "找不到文件。",
|
||||
"connection": "无法连接到服务器。"
|
||||
"connection": "无法连接到服务器。",
|
||||
"shareNotFound": "无效或过期共享。"
|
||||
},
|
||||
"files": {
|
||||
"body": "内容",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "已添加访问规则!",
|
||||
"name": "名称",
|
||||
"accessManagement": "访问管理",
|
||||
"all": "全部拒绝"
|
||||
"all": "全部拒绝",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "这显示了当用户试图访问没有特定访问规则的路径时会发生什么。允许 \"表示默认情况下用户可以访问,\"拒绝 \"表示除非明确允许,否则默认情况下用户将被阻止。"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "编辑器初始化失败。请重新加载。",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "所有履歷",
|
||||
"clearCompleted": "清除完成",
|
||||
"showMore": "顯示更多",
|
||||
"showLess": "顯示更少"
|
||||
"showLess": "顯示更少",
|
||||
"openParentFolder": "開啟父資料夾"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下載檔案",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "您無權訪問。",
|
||||
"internal": "伺服器出了點問題。",
|
||||
"notFound": "找不到檔案。",
|
||||
"connection": "伺服器無法連線。"
|
||||
"connection": "伺服器無法連線。",
|
||||
"shareNotFound": "無效或過期的共享。"
|
||||
},
|
||||
"files": {
|
||||
"body": "内容",
|
||||
@@ -510,7 +512,9 @@
|
||||
"added": "已新增存取規則!",
|
||||
"name": "名稱",
|
||||
"accessManagement": "存取管理",
|
||||
"all": "全部拒絕"
|
||||
"all": "全部拒絕",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "這顯示使用者嘗試存取沒有特定存取規則的路徑時會發生的情況。允許」表示使用者預設可以存取,「拒絕」表示使用者預設會被阻擋,除非明確允許。"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "編輯器初始化失敗。請重新載入。",
|
||||
|
||||
@@ -24,6 +24,7 @@ const titles = {
|
||||
User: "settings.user",
|
||||
Forbidden: "errors.forbidden",
|
||||
NotFound: "errors.notFound",
|
||||
ShareNotFound: "errors.shareNotFound",
|
||||
InternalServerError: "errors.internal",
|
||||
};
|
||||
|
||||
@@ -137,7 +138,7 @@ const router = createRouter({
|
||||
|
||||
// Helper function to check if a route resolves to itself
|
||||
function isSameRoute(to: RouteLocation, from: RouteLocation) {
|
||||
return to.path === from.path && to.hash === from.hash;
|
||||
return to.path === from.path;
|
||||
}
|
||||
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { removePrefix, buildItemUrl, removeLeadingSlash } from '@/utils/url.js'
|
||||
import { getFileExtension } from '@/utils/files.js'
|
||||
import { state, mutations } from '@/store'
|
||||
import { noAuth, shareOverrides } from '@/utils/constants.js'
|
||||
import { noAuth, shareInfo } from '@/utils/constants.js'
|
||||
import { getTypeInfo } from '@/utils/mimetype'
|
||||
import { fromNow } from '@/utils/moment'
|
||||
import * as i18n from '@/i18n'
|
||||
@@ -36,12 +36,12 @@ export const getters = {
|
||||
isCardView: () =>
|
||||
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
|
||||
getters.currentView() == 'listingView',
|
||||
currentHash: () => state.route.hash.replace('#', ''),
|
||||
currentHash: () => shareInfo.hash,
|
||||
isMobile: () => state.isMobile,
|
||||
isLoading: () => Object.keys(state.loading).length > 0,
|
||||
isSettings: () => getters.currentView() === 'settings',
|
||||
isShare: () => {
|
||||
return shareOverrides.isShare
|
||||
return shareInfo.isShare
|
||||
},
|
||||
isDarkMode: () => {
|
||||
if (state.user == null) {
|
||||
@@ -137,7 +137,7 @@ export const getters = {
|
||||
return { dirs, files }
|
||||
},
|
||||
isSidebarVisible: () => {
|
||||
if (shareOverrides.disableSidebar) {
|
||||
if (shareInfo.disableSidebar) {
|
||||
return false
|
||||
}
|
||||
const cv = getters.currentView()
|
||||
@@ -190,16 +190,14 @@ export const getters = {
|
||||
return removePrefix(state.route.path, trimModifier)
|
||||
},
|
||||
shareHash: () => {
|
||||
let urlPath = getters.routePath('public/share')
|
||||
let parts = urlPath.split('/')
|
||||
return parts[1]
|
||||
return shareInfo.hash
|
||||
},
|
||||
sharePathBase: () => {
|
||||
return '/public/share/' + getters.shareHash() + '/'
|
||||
return '/public/share/' + shareInfo.hash + '/'
|
||||
},
|
||||
getSharePath: (subPath = "") => {
|
||||
let urlPath = getters.routePath('public/share')
|
||||
let path = "/" + removeLeadingSlash(urlPath.split(state.share.hash)[1])
|
||||
let path = "/" + removeLeadingSlash(urlPath.split(shareInfo.hash)[1])
|
||||
if (subPath != "") {
|
||||
path += "/" + removeLeadingSlash(subPath)
|
||||
}
|
||||
@@ -344,7 +342,7 @@ export const getters = {
|
||||
return false
|
||||
},
|
||||
officeViewingDisabled: filename => {
|
||||
if (shareOverrides.isShare) {
|
||||
if (shareInfo.isShare) {
|
||||
return true
|
||||
}
|
||||
const ext = ' ' + getFileExtension(filename)
|
||||
@@ -429,4 +427,10 @@ export const getters = {
|
||||
}
|
||||
return "close";
|
||||
},
|
||||
isInvalidShare: () => {
|
||||
return shareInfo.isShare && !shareInfo.isValid;
|
||||
},
|
||||
isValidShare: () => {
|
||||
return shareInfo.isShare && shareInfo.isValid;
|
||||
},
|
||||
};
|
||||
@@ -427,6 +427,12 @@ export const mutations = {
|
||||
state.isSearchActive = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
resetAll: () => {
|
||||
state.isSearchActive = false;
|
||||
state.selected = [];
|
||||
state.req = {};
|
||||
emitStateChanged();
|
||||
},
|
||||
showTooltip(value) {
|
||||
state.tooltip.content = value.content;
|
||||
state.tooltip.x = value.x;
|
||||
|
||||
@@ -23,7 +23,7 @@ const muPdfAvailable = window.filebrowser.muPdfAvailable;
|
||||
const updateAvailable = window.filebrowser.updateAvailable;
|
||||
const disableNavButtons = window.filebrowser.disableNavButtons;
|
||||
const userSelectableThemes = window.filebrowser.userSelectableThemes;
|
||||
const shareOverrides = window.filebrowser.share;
|
||||
const shareInfo = window.filebrowser.share;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -37,7 +37,7 @@ const settings = [
|
||||
];
|
||||
|
||||
export {
|
||||
shareOverrides,
|
||||
shareInfo,
|
||||
userSelectableThemes,
|
||||
disableNavButtons,
|
||||
updateAvailable,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { baseURL } from "@/utils/constants.js";
|
||||
import { state, mutations } from "@/store";
|
||||
import { router } from "@/router";
|
||||
import { shareInfo } from "@/utils/constants.js";
|
||||
|
||||
export function removeLastDir(url) {
|
||||
var arr = url.split("/");
|
||||
@@ -48,6 +49,7 @@ export default {
|
||||
removePrefix,
|
||||
getApiPath,
|
||||
extractSourceFromPath,
|
||||
fixDownloadURL,
|
||||
};
|
||||
|
||||
export function removePrefix(path, prefix = "") {
|
||||
@@ -155,15 +157,15 @@ export function encodedPath(path) {
|
||||
}
|
||||
|
||||
// assume non-encoded input path and source
|
||||
export function goToItem(source, path, previousHash, shareHash) {
|
||||
mutations.replaceRequest({})
|
||||
export function goToItem(source, path, previousHash) {
|
||||
mutations.resetAll()
|
||||
if (previousHash) {
|
||||
location.hash = previousHash;
|
||||
}
|
||||
let newPath = encodedPath(path);
|
||||
let fullPath;
|
||||
if (shareHash) {
|
||||
fullPath = `/public/share/${shareHash}${newPath}`;
|
||||
if (shareInfo.isShare) {
|
||||
fullPath = `/public/share/${shareInfo.hash}${newPath}`;
|
||||
router.push({ path: fullPath });
|
||||
return;
|
||||
}
|
||||
@@ -178,4 +180,29 @@ export function goToItem(source, path, previousHash, shareHash) {
|
||||
|
||||
export function doubleEncode(str) {
|
||||
return encodeURIComponent(encodeURIComponent(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes download URLs by replacing everything before /public/api
|
||||
* with the current window.location.origin + baseURL
|
||||
* @param {string} downloadUrl - The original download URL from backend
|
||||
* @returns {string} - The corrected URL using current client origin
|
||||
*/
|
||||
export function fixDownloadURL(downloadUrl) {
|
||||
if (!downloadUrl) {
|
||||
return downloadUrl;
|
||||
}
|
||||
// Find the position of /public/api in the URL
|
||||
const publicApiIndex = downloadUrl.indexOf('/public/api');
|
||||
if (publicApiIndex === -1) {
|
||||
// If /public/api is not found, return the original URL
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
// Extract the part from /public/api onwards
|
||||
const publicApiPath = downloadUrl.substring(publicApiIndex);
|
||||
|
||||
// Build the corrected URL using current client origin and baseURL
|
||||
const correctedBaseURL = removeTrailingSlash(baseURL);
|
||||
return `${window.location.origin}${correctedBaseURL}${publicApiPath}`;
|
||||
}
|
||||
@@ -21,6 +21,10 @@ const errors = {
|
||||
icon: "gps_off",
|
||||
message: "errors.notFound",
|
||||
},
|
||||
"share404": {
|
||||
icon: "gps_off",
|
||||
message: "errors.shareNotFound",
|
||||
},
|
||||
500: {
|
||||
icon: "error_outline",
|
||||
message: "errors.internal",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Share Info Component -->
|
||||
<ShareInfo
|
||||
<ShareInfoCard
|
||||
v-if="showShareInfo"
|
||||
class="share-info-component"
|
||||
:hash="share?.hash"
|
||||
@@ -41,10 +41,10 @@ import MarkdownViewer from "./files/MarkdownViewer.vue";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
import router from "@/router";
|
||||
import { baseURL, shareOverrides } from "@/utils/constants";
|
||||
import { baseURL, shareInfo } from "@/utils/constants";
|
||||
import PopupPreview from "@/components/files/PopupPreview.vue";
|
||||
import { extractSourceFromPath } from "@/utils/url";
|
||||
import ShareInfo from "@/components/files/ShareInfo.vue";
|
||||
import ShareInfoCard from "@/components/files/ShareInfoCard.vue";
|
||||
|
||||
export default {
|
||||
name: "files",
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
OnlyOfficeEditor,
|
||||
MarkdownViewer,
|
||||
PopupPreview,
|
||||
ShareInfo,
|
||||
ShareInfoCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
return state.share;
|
||||
},
|
||||
showShareInfo() {
|
||||
return this.isShare && state.share.hash && state.isMobile && state.req.path == "/" && !shareOverrides.disableShareCard;
|
||||
return this.isShare && state.share.hash && state.isMobile && state.req.path == "/" && !shareInfo.disableShareCard;
|
||||
},
|
||||
isShare() {
|
||||
return getters.isShare();
|
||||
@@ -116,6 +116,13 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("hashchange", this.scrollToHash);
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
if (getters.isInvalidShare()) {
|
||||
// show message that share is invalid and don't do anything else
|
||||
this.error = {
|
||||
status: "share404",
|
||||
message: "errors.shareNotFound",
|
||||
};
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
@@ -153,7 +160,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async fetchData() {
|
||||
if (state.deletedItem) {
|
||||
if (state.deletedItem || getters.isInvalidShare()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import { filesApi } from "@/api";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { events } from "@/notify";
|
||||
import { generateRandomCode } from "@/utils/auth";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "layout",
|
||||
@@ -65,8 +65,8 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateIsMobile);
|
||||
if (shareOverrides.themeColor != "") {
|
||||
document.documentElement.style.setProperty("--primaryColor", shareOverrides.themeColor);
|
||||
if (shareInfo.themeColor != "") {
|
||||
document.documentElement.style.setProperty("--primaryColor", shareInfo.themeColor);
|
||||
} else if (state.user.themeColor) {
|
||||
document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
|
||||
}
|
||||
|
||||
@@ -81,14 +81,68 @@ export default {
|
||||
return active;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Watch for route hash changes
|
||||
"$route.hash"() {
|
||||
this.initializeActiveSettingFromHash();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
this.initializeActiveSettingFromHash();
|
||||
|
||||
// Listen for hash changes (browser navigation)
|
||||
window.addEventListener('hashchange', this.handleHashChange);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up event listener
|
||||
window.removeEventListener('hashchange', this.handleHashChange);
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {any} setting
|
||||
*/
|
||||
shouldShow(setting) {
|
||||
const perm = setting?.permissions || {};
|
||||
return Object.keys(perm).every((key) => state.user.permissions[key]);
|
||||
const userPermissions = /** @type {Record<string, boolean>} */ (state.user.permissions || {});
|
||||
return Object.keys(perm).every((key) => userPermissions[key]);
|
||||
},
|
||||
handleHashChange() {
|
||||
// Handle browser back/forward navigation
|
||||
this.initializeActiveSettingFromHash();
|
||||
},
|
||||
initializeActiveSettingFromHash() {
|
||||
// Get the current hash from the URL
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
|
||||
if (hash) {
|
||||
// Check if the hash corresponds to a valid setting
|
||||
const validSetting = this.settings.find(
|
||||
(setting) => `${setting.id}-main` === hash && this.shouldShow(setting)
|
||||
);
|
||||
|
||||
if (validSetting) {
|
||||
// Set the active settings view to the hash value
|
||||
mutations.setActiveSettingsView(hash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to profile-main if no hash or invalid hash
|
||||
const defaultSetting = this.settings.find(
|
||||
(setting) => setting.id === 'profile' && this.shouldShow(setting)
|
||||
);
|
||||
|
||||
if (defaultSetting) {
|
||||
mutations.setActiveSettingsView('profile-main');
|
||||
} else {
|
||||
// Fallback to first allowed setting if profile is not available
|
||||
const firstAllowed = this.settings.find((setting) => this.shouldShow(setting));
|
||||
if (firstAllowed) {
|
||||
mutations.setActiveSettingsView(`${firstAllowed.id}-main`);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ import { eventBus } from "@/store/eventBus";
|
||||
import { getters, state, mutations } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
import { disableNavButtons, shareOverrides } from "@/utils/constants";
|
||||
import { disableNavButtons, shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "UnifiedHeader",
|
||||
@@ -59,8 +59,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
getTopTitle() {
|
||||
if (getters.isShare() && shareOverrides.title) {
|
||||
return shareOverrides.title;
|
||||
if (getters.isShare() && shareInfo.title) {
|
||||
return shareInfo.title;
|
||||
}
|
||||
return state.req.name;
|
||||
},
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
return state.isSearchActive || getters.currentPromptName() != "";
|
||||
},
|
||||
isDisabledMultiAction() {
|
||||
const shareDisabled = shareOverrides.disableSidebar && getters.multibuttonState() === "menu";
|
||||
const shareDisabled = shareInfo.disableSidebar && getters.multibuttonState() === "menu";
|
||||
return this.isDisabled || (getters.isStickySidebar() && getters.multibuttonState() === "menu") || shareDisabled;
|
||||
},
|
||||
showSwitchView() {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
|
||||
<div class="preview" v-if="!isDeleted">
|
||||
<ExtendedImage v-if="previewType == 'image' || pdfConvertable" :src="raw"
|
||||
<ExtendedImage v-if="previewType == 'image' || pdfConvertable && !disableFileViewer" :src="raw"
|
||||
@navigate-previous="prev"
|
||||
@navigate-next="next">
|
||||
</ExtendedImage>
|
||||
<audio v-else-if="previewType == 'audio'" ref="player" :src="raw" controls :autoplay="autoPlay"
|
||||
<audio v-else-if="previewType == 'audio' && !disableFileViewer" ref="player" :src="raw" controls :autoplay="autoPlay"
|
||||
@play="autoPlay = true"></audio>
|
||||
<video v-else-if="previewType == 'video'" ref="player" :src="raw" controls :autoplay="autoPlay"
|
||||
<video v-else-if="previewType == 'video' && !disableFileViewer" ref="player" :src="raw" controls :autoplay="autoPlay"
|
||||
@play="autoPlay = true">
|
||||
<track kind="captions" v-for="(sub, index) in subtitlesList" :key="index" :src="sub.src"
|
||||
:label="'Subtitle ' + sub.name" :default="index === 0" />
|
||||
</video>
|
||||
|
||||
<div v-else-if="previewType == 'pdf'" class="pdf-wrapper">
|
||||
<div v-else-if="previewType == 'pdf' && !disableFileViewer" class="pdf-wrapper">
|
||||
<iframe class="pdf" :src="raw"></iframe>
|
||||
<a v-if="isMobileSafari" :href="raw" target="_blank" class="button button--flat floating-btn">
|
||||
<div>
|
||||
@@ -65,6 +65,7 @@ import { getFileExtension } from "@/utils/files";
|
||||
import { convertToVTT } from "@/utils/subtitles";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { muPdfAvailable } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
export default {
|
||||
name: "preview",
|
||||
components: {
|
||||
@@ -140,8 +141,7 @@ export default {
|
||||
path: state.share.subPath,
|
||||
hash: state.share.hash,
|
||||
token: state.share.token,
|
||||
inline: true,
|
||||
}, [state.req.path]);
|
||||
}, [state.req.path], true);
|
||||
}
|
||||
return filesApi.getDownloadURL(state.req.source, state.req.path, true);
|
||||
},
|
||||
@@ -173,6 +173,9 @@ export default {
|
||||
deletedItem() {
|
||||
return state.deletedItem;
|
||||
},
|
||||
disableFileViewer() {
|
||||
return shareInfo.disableFileViewer;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
deletedItem() {
|
||||
@@ -315,7 +318,7 @@ export default {
|
||||
});
|
||||
}
|
||||
const directoryPath = url.removeLastDir(state.req.path);
|
||||
if (!this.listing) {
|
||||
if (!this.listing || this.listing == "undefined") {
|
||||
let res;
|
||||
if (getters.isShare()) {
|
||||
// Use public API for shared files
|
||||
@@ -324,7 +327,9 @@ export default {
|
||||
// Use regular files API for authenticated users
|
||||
res = await filesApi.fetchFiles(state.req.source, directoryPath);
|
||||
}
|
||||
this.listing = res.items;
|
||||
}
|
||||
if (!this.listing) {
|
||||
this.listing = [state.req]
|
||||
}
|
||||
this.name = state.req.name;
|
||||
this.previousLink = "";
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="settings-items">
|
||||
@@ -51,6 +52,12 @@
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button class="action copy-clipboard" :data-clipboard-text="fixDownloadURL(link.downloadURL)" v-if="link.downloadURL"
|
||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')">
|
||||
<i class="material-icons">content_paste_go</i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -67,7 +74,9 @@ import { publicApi } from "@/api";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import Clipboard from "clipboard";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
import { fromNow } from '@/utils/moment'
|
||||
import { fromNow } from '@/utils/moment';
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import { fixDownloadURL } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "shares",
|
||||
@@ -76,35 +85,31 @@ export default {
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
/** @type {any} */
|
||||
error: null,
|
||||
/** @type {any[]} */
|
||||
links: [],
|
||||
/** @type {any} */
|
||||
clip: null,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
mutations.setLoading("shares", true);
|
||||
try {
|
||||
let links = await publicApi.list();
|
||||
if (links.length === 0) {
|
||||
this.links = [];
|
||||
return;
|
||||
}
|
||||
this.links = links;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
notify.showError(e);
|
||||
} finally {
|
||||
mutations.setLoading("shares", false);
|
||||
}
|
||||
await this.reloadShares();
|
||||
},
|
||||
mounted() {
|
||||
this.clip = new Clipboard(".copy-clipboard");
|
||||
this.clip.on("success", () => {
|
||||
notify.showSuccess(this.$t("success.linkCopied"));
|
||||
});
|
||||
|
||||
// Listen for share changes
|
||||
eventBus.on('sharesChanged', this.reloadShares);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.clip.destroy();
|
||||
|
||||
// Clean up event listener
|
||||
eventBus.removeEventListener('sharesChanged', this.reloadShares);
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
@@ -121,6 +126,26 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async reloadShares() {
|
||||
mutations.setLoading("shares", true);
|
||||
try {
|
||||
let links = await publicApi.list();
|
||||
if (links.length === 0) {
|
||||
this.links = [];
|
||||
return;
|
||||
}
|
||||
this.links = links;
|
||||
this.error = null; // Clear any previous errors
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
notify.showError(e);
|
||||
} finally {
|
||||
mutations.setLoading("shares", false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {any} link
|
||||
*/
|
||||
editLink(link) {
|
||||
mutations.showHover({
|
||||
name: "share",
|
||||
@@ -130,6 +155,10 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {any} event
|
||||
* @param {any} link
|
||||
*/
|
||||
deleteLink: async function (event, link) {
|
||||
mutations.showHover({
|
||||
name: "share-delete",
|
||||
@@ -147,12 +176,21 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {any} time
|
||||
*/
|
||||
humanTime(time) {
|
||||
return fromNow(time);
|
||||
},
|
||||
/**
|
||||
* @param {any} share
|
||||
*/
|
||||
buildLink(share) {
|
||||
return publicApi.getShareURL(share);
|
||||
},
|
||||
fixDownloadURL(downloadUrl) {
|
||||
return fixDownloadURL(downloadUrl);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -22,9 +22,58 @@ export function setupErrorTracking(page: Page) {
|
||||
const failedResponses: { url: string; status: number }[] = [];
|
||||
|
||||
// Track console errors
|
||||
page.on("console", (message) => {
|
||||
page.on("console", async (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
const errorText = message.text();
|
||||
const args = message.args();
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let detailedError = errorText;
|
||||
|
||||
if (args.length > 0) {
|
||||
try {
|
||||
// Get the first argument which usually contains the error object
|
||||
const firstArg = await args[0].jsonValue().catch(() => null);
|
||||
|
||||
if (firstArg && typeof firstArg === 'object') {
|
||||
if (firstArg.stack) {
|
||||
// If we have a stack trace, use it
|
||||
detailedError = firstArg.stack;
|
||||
} else if (firstArg.message) {
|
||||
// Otherwise use the message if available
|
||||
detailedError = `${firstArg.name || 'Error'}: ${firstArg.message}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't extract detailed info, try to get string representation of args
|
||||
try {
|
||||
const argsText = await Promise.all(
|
||||
args.map(async (arg) => {
|
||||
try {
|
||||
return await arg.evaluate((obj) => {
|
||||
if (obj && typeof obj === 'object' && obj.stack) {
|
||||
return obj.stack;
|
||||
}
|
||||
return String(obj);
|
||||
});
|
||||
} catch {
|
||||
return '[Unable to serialize]';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const combinedArgs = argsText.join(' ');
|
||||
if (combinedArgs.trim() && combinedArgs !== errorText) {
|
||||
detailedError = combinedArgs;
|
||||
}
|
||||
} catch {
|
||||
// Fallback to original text
|
||||
detailedError = errorText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consoleErrors.push(detailedError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,11 +90,21 @@ export function setupErrorTracking(page: Page) {
|
||||
return {
|
||||
checkForErrors: (expectedConsoleErrors = 0, expectedApiErrors = 0) => {
|
||||
if (consoleErrors.length !== expectedConsoleErrors) {
|
||||
console.error("Unexpected Console Errors:", consoleErrors);
|
||||
console.error(`\n=== Unexpected Console Errors (Expected: ${expectedConsoleErrors}, Got: ${consoleErrors.length}) ===`);
|
||||
consoleErrors.forEach((error, index) => {
|
||||
console.error(`\nError ${index + 1}:`);
|
||||
console.error(error);
|
||||
console.error('---');
|
||||
});
|
||||
console.error('=== End Console Errors ===\n');
|
||||
}
|
||||
|
||||
if (failedResponses.length !== expectedApiErrors) {
|
||||
console.error("Unexpected Failed API Calls:", failedResponses);
|
||||
console.error(`\n=== Unexpected Failed API Calls (Expected: ${expectedApiErrors}, Got: ${failedResponses.length}) ===`);
|
||||
failedResponses.forEach((response, index) => {
|
||||
console.error(`\nFailed Request ${index + 1}: ${response.status} - ${response.url}`);
|
||||
});
|
||||
console.error('=== End Failed API Calls ===\n');
|
||||
}
|
||||
|
||||
expect(consoleErrors).toHaveLength(expectedConsoleErrors);
|
||||
|
||||
@@ -61,6 +61,9 @@ test-frontend:
|
||||
|
||||
test-playwright: build-frontend
|
||||
cd backend && GOOS=linux go build -o filebrowser .
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-general .
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-sharing .
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-noauth .
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-proxy .
|
||||
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-oidc .
|
||||
|
||||
|
||||
Reference in new issue
Block a user