Beta/v0.8.1 (#1106)

This commit is contained in:
Graham Steffaniak authored and GitHub committed 2025-08-22 17:45:44 -05:00
1 parent 7d0707ebe0
commit 5b0f388fa5
78 files changed
+1856 -618

No files matched your search

+26
View File
@@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.8.1-beta
**New Features**:
- api for generate download link (see swagger) https://github.com/gtsteffaniak/filebrowser/issues/1007
- added `source.config.disabled` option to disable a source without removing it from config file.
- added `source.config.private` option to designate as private -- currently just means no sharing permitted.
- hide share card in share
- download count for a share shows up on share management
**Notes**:
- updated description for indexingIntervalMinutes https://github.com/gtsteffaniak/filebrowser/issues/1067
**BugFixes**:
- fixed styling issues https://github.com/gtsteffaniak/filebrowser/issues/1086 https://github.com/gtsteffaniak/filebrowser/issues/1081 https://github.com/gtsteffaniak/filebrowser/issues/1082 https://github.com/gtsteffaniak/filebrowser/issues/1098
- fix download limit issue https://github.com/gtsteffaniak/filebrowser/issues/1085
- fixed oidc user defaults for new user https://github.com/gtsteffaniak/filebrowser/issues/1071
- shares get updated when files moved in ui https://github.com/gtsteffaniak/filebrowser/issues/760
- click listing behavior doesn't clear (introduced in 0.8.0) https://github.com/gtsteffaniak/filebrowser/issues/1101
- show download count and limit in share list in settings https://github.com/gtsteffaniak/filebrowser/issues/1103
- fix windows alt+arrow movement issue https://github.com/gtsteffaniak/filebrowser/issues/1094
- nav memory issue for filenames with brackets https://github.com/gtsteffaniak/filebrowser/issues/1092
- files with "+"" in name issue https://github.com/gtsteffaniak/filebrowser/issues/1089
- fixed editor bug in share view https://github.com/gtsteffaniak/filebrowser/issues/1084
- other share related issues https://github.com/gtsteffaniak/filebrowser/issues/1087 https://github.com/gtsteffaniak/filebrowser/issues/1064
## v0.8.0-beta
This is a major release, new features and changes could introduce breaking behavior. Here are the known potentially breaking changes:
+1
View File
@@ -11,6 +11,7 @@ server:
- path: "."
name: "docker"
config:
private: true
defaultEnabled: true
frontend:
name: "Graham's Filebrowser"
+21 -6
View File
@@ -22,6 +22,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"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/indexing"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
"github.com/gtsteffaniak/go-logger/logger"
@@ -73,7 +74,7 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
}
response.FileInfo = *info
response.RealPath = realPath
response.Source = opts.Source
response.Source = index.Name
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
response.OnlyOfficeId = generateOfficeId(realPath)
}
@@ -188,7 +189,7 @@ func validateMoveDestination(src, dst string, isSrcDir bool) error {
return nil
}
func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string) error {
func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string, s *share.Storage) error {
// Validate the move operation before executing
if err := validateMoveDestination(realsrc, realdst, isSrcDir); err != nil {
return err
@@ -198,8 +199,21 @@ func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
if err != nil {
return err
}
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
go RefreshIndex(sourceIndex, realdst, isDestDir) //nolint:errcheck
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
go RefreshIndex(destIndex, realdst, isDestDir) //nolint:errcheck
// update shares
idx := indexing.GetIndex(sourceIndex)
if idx == nil {
return fmt.Errorf("could not get index: %v ", sourceIndex)
}
idx2 := indexing.GetIndex(destIndex)
if idx2 == nil {
return fmt.Errorf("could not get index: %v ", destIndex)
}
// Use backend source paths to match how shares are stored
go s.UpdateShares(idx.Path, idx.MakeIndexPath(realsrc), idx2.Path, idx2.MakeIndexPath(realdst)) //nolint:errcheck
return nil
}
@@ -213,8 +227,9 @@ func CopyResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
if err != nil {
return err
}
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
go RefreshIndex(sourceIndex, realdst, isDestDir) //nolint:errcheck
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
go RefreshIndex(destIndex, realdst, isDestDir) //nolint:errcheck
// update shares
return nil
}
+3
View File
@@ -98,6 +98,9 @@ func setupSources(generate bool) {
logger.Fatal("There are no `server.sources` configured. If you have `server.root` configured, please update the config and add at least one `server.sources` with a `path` configured.")
} else {
for k, source := range Config.Server.Sources {
if source.Config.Disabled {
continue
}
realPath := getRealPath(source.Path)
name := filepath.Base(realPath)
if name == "\\" {
+3 -1
View File
@@ -77,7 +77,9 @@ type Source struct {
}
type SourceConfig struct {
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in seconds to re-index the source
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
Disabled bool `json:"disabled"` // disable the source, this is useful so you don't need to remove it from the config file
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in minutes to re-index the source
DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
+8 -2
View File
@@ -63,8 +63,8 @@ type GroupMap map[string]StringSet
// Storage manages access rules and group membership.
type Storage struct {
mux sync.RWMutex
AllRules SourceRuleMap // AllRules[sourcePath][indexPath]
Groups GroupMap // key: group name, value: set of usernames
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
DB *storm.DB // Optional: DB for persistence
Users *users.Storage // Reference to users storage
}
@@ -84,6 +84,12 @@ func (s *Storage) SaveToDB() error {
return s.DB.Set(accessRulesBucket, accessRulesKey, data)
}
// Flush persists the current in-memory state to the backing store.
// Call during graceful shutdown to ensure DB matches memory.
func (s *Storage) Flush() error {
return s.SaveToDB()
}
// LoadFromDB loads all rules from the DB if DB is set.
func (s *Storage) LoadFromDB() error {
if s.DB == nil {
+11 -8
View File
@@ -20,25 +20,28 @@ type CommonShare struct {
Favicon string `json:"favicon,omitempty"`
QuickDownload bool `json:"quickDownload,omitempty"`
HideNavButtons bool `json:"hideNavButtons,omitempty"`
DisableSidebar bool `json:"disableSidebar"`
ViewMode string `json:"viewMode,omitempty"`
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
Path string `json:"path,omitempty"`
DownloadURL string `json:"downloadURL,omitempty"`
DisableShareCard bool `json:"disableShareCard,omitempty"`
Username string `json:"username,omitempty"`
}
type CreateBody struct {
CommonShare
Hash string `json:"hash,omitempty"`
SourceName string `json:"sourceName,omitempty"`
Password string `json:"password"`
Expires string `json:"expires"`
Unit string `json:"unit"`
Hash string `json:"hash,omitempty"`
Password string `json:"password"`
Expires string `json:"expires"`
Unit string `json:"unit"`
}
// Link is the information needed to build a shareable link.
type Link struct {
CommonShare
Mu sync.Mutex `json:"-"`
Downloads int `json:"-"`
Downloads int `json:"downloads"`
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Source string `json:"source" storm:"index"`
UserID uint `json:"userID"`
Expire int64 `json:"expire"`
PasswordHash string `json:"password_hash,omitempty"`
+224 -11
View File
@@ -1,10 +1,14 @@
package share
import (
"strings"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/database/crud"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
"github.com/gtsteffaniak/go-logger/logger"
)
// StorageBackend is the interface to implement for a share storage.
@@ -13,6 +17,7 @@ type StorageBackend interface {
FindByUserID(id uint) ([]*Link, error)
GetByHash(hash string) (*Link, error)
GetPermanent(path, source string, id uint) (*Link, error)
GetBySourcePath(path, source string) ([]*Link, error)
Gets(path, source string, id uint) ([]*Link, error)
Save(s *Link) error
Delete(hash string) error
@@ -49,15 +54,31 @@ func (c *crudBackend) DeleteByID(id any) error {
// Storage is a share storage using generics.
type Storage struct {
Generic *crud.Storage[Link]
back StorageBackend
Generic *crud.Storage[Link]
back StorageBackend
shareCache map[string]*Link
mu sync.RWMutex
users *users.Storage
}
// NewStorage creates a share links storage from a backend.
func NewStorage(back StorageBackend) *Storage {
func NewStorage(back StorageBackend, usersStore *users.Storage) *Storage {
return &Storage{
Generic: crud.NewStorage[Link](&crudBackend{back: back}),
back: back,
Generic: crud.NewStorage[Link](&crudBackend{back: back}),
back: back,
shareCache: make(map[string]*Link),
users: usersStore,
}
}
// 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
}
}
@@ -67,7 +88,27 @@ func (s *Storage) All() ([]*Link, error) {
if err != nil {
return nil, err
}
return s.filterExpired(links)
filtered, err := s.filterExpired(links)
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 {
continue
}
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
filtered[i] = existing
} else {
s.shareCache[l.Hash] = l
}
}
s.mu.Unlock()
return filtered, nil
}
// FindByUserID wraps StorageBackend.FindByUserID and handles expiry.
@@ -76,11 +117,46 @@ func (s *Storage) FindByUserID(id uint) ([]*Link, error) {
if err != nil {
return nil, err
}
return s.filterExpired(links)
filtered, err := s.filterExpired(links)
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 {
continue
}
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
filtered[i] = existing
} else {
s.shareCache[l.Hash] = l
}
}
s.mu.Unlock()
return filtered, nil
}
// GetByHash wraps StorageBackend.GetByHash and handles expiry.
func (s *Storage) GetByHash(hash string) (*Link, error) {
// return stable in-memory pointer if available
s.mu.RLock()
if link, ok := s.shareCache[hash]; ok && link != nil {
s.mu.RUnlock()
if link.Expire != 0 && link.Expire <= time.Now().Unix() {
_ = s.back.Delete(hash)
s.mu.Lock()
delete(s.shareCache, hash)
s.mu.Unlock()
return nil, errors.ErrNotExist
}
return link, nil
}
s.mu.RUnlock()
link, err := s.back.GetByHash(hash)
if err != nil {
return nil, err
@@ -89,12 +165,23 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
_ = s.back.Delete(hash)
return nil, errors.ErrNotExist
}
s.mu.Lock()
s.setUsername(link)
s.shareCache[hash] = link
s.mu.Unlock()
return link, nil
}
// GetPermanent wraps StorageBackend.GetPermanent
func (s *Storage) GetPermanent(path, source string, id uint) (*Link, error) {
return s.back.GetPermanent(path, source, id)
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()
}
return l, err
}
// Gets wraps StorageBackend.Gets and handles expiry.
@@ -103,17 +190,140 @@ func (s *Storage) Gets(sourcePath, source string, id uint) ([]*Link, error) {
if err != nil {
return nil, err
}
return s.filterExpired(links)
filtered, err := s.filterExpired(links)
if err != nil {
return nil, err
}
for _, l := range filtered {
s.setUsername(l)
}
s.mu.Lock()
for i, l := range filtered {
if l == nil {
continue
}
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
filtered[i] = existing
} else {
s.shareCache[l.Hash] = l
}
}
s.mu.Unlock()
return filtered, nil
}
// GetBySourcePath wraps StorageBackend.GetBySourcePath and handles expiry.
func (s *Storage) GetBySourcePath(path, source string) ([]*Link, error) {
links, err := s.back.GetBySourcePath(path, source)
if err != nil {
return nil, err
}
filtered, err := s.filterExpired(links)
if err != nil {
return nil, err
}
for _, l := range filtered {
s.setUsername(l)
}
s.mu.Lock()
for i, l := range filtered {
if l == nil {
continue
}
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
filtered[i] = existing
} else {
s.shareCache[l.Hash] = l
}
}
s.mu.Unlock()
return filtered, nil
}
// UpdateShares updates all shares that exactly match oldSource and oldPath
// to point to newSource and newPath. Returns the number of updated shares.
func (s *Storage) UpdateShares(oldSource, oldPath, newSource, newPath string) (int, error) {
links, err := s.back.All()
if err != nil && err != errors.ErrNotExist {
logger.Error("failed to list shares", "error", err)
return 0, err
}
updated := 0
for _, l := range links {
if l == nil {
continue
}
if l.Source != oldSource {
continue
}
pos := strings.Index(l.Path, oldPath)
if pos < 0 {
continue
}
oldFullPath := l.Path
l.Source = newSource
l.Path = oldFullPath[:pos] + newPath + oldFullPath[pos+len(oldPath):]
if err := s.back.Save(l); err != nil {
logger.Error("failed to save updated share", "hash", l.Hash, "error", err)
return updated, err
}
s.mu.Lock()
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
// update existing cached pointer fields
existing.Source = l.Source
existing.Path = l.Path
} else {
s.shareCache[l.Hash] = l
}
s.mu.Unlock()
logger.Info("share updated", "hash", l.Hash, "fromPath", oldFullPath, "toPath", l.Path)
updated++
}
if updated == 0 {
logger.Warning("no matching shares to update for provided source/path")
}
return updated, nil
}
// Save wraps StorageBackend.Save
func (s *Storage) Save(l *Link) error {
return s.back.Save(l)
if err := s.back.Save(l); err != nil {
return err
}
s.mu.Lock()
s.shareCache[l.Hash] = l
s.mu.Unlock()
return nil
}
// Delete wraps StorageBackend.Delete
func (s *Storage) Delete(hash string) error {
return s.back.Delete(hash)
if err := s.back.Delete(hash); err != nil {
return err
}
s.mu.Lock()
delete(s.shareCache, hash)
s.mu.Unlock()
return nil
}
// Flush persists the current in-memory state of all shares to the backing store.
// Call during graceful shutdown to ensure DB matches memory.
func (s *Storage) Flush() error {
s.mu.RLock()
links := make([]*Link, 0, len(s.shareCache))
for _, l := range s.shareCache {
if l != nil {
links = append(links, l)
}
}
s.mu.RUnlock()
for _, l := range links {
if err := s.back.Save(l); err != nil {
return err
}
}
return nil
}
// filterExpired removes expired links and deletes them from storage.
@@ -122,6 +332,9 @@ func (s *Storage) filterExpired(links []*Link) ([]*Link, error) {
for _, link := range links {
if link.Expire != 0 && link.Expire <= time.Now().Unix() && !link.KeepAfterExpiration {
_ = s.back.Delete(link.Hash)
s.mu.Lock()
delete(s.shareCache, link.Hash)
s.mu.Unlock()
continue
}
filtered = append(filtered, link)
+1 -1
View File
@@ -29,7 +29,7 @@ func NewStorage(db *storm.DB) (*BoltStore, error) {
}
return &BoltStore{
Users: userStore,
Share: share.NewStorage(shareBackend{db: db}),
Share: share.NewStorage(shareBackend{db: db}, userStore),
Auth: authStore,
Settings: settings.NewStorage(settingsBackend{db: db}),
Access: access.NewStorage(db, userStore),
+11 -1
View File
@@ -52,6 +52,16 @@ func (s shareBackend) GetPermanent(path, source string, id uint) (*share.Link, e
return &v, nil
}
// GetBySourcePath returns all shares that exactly match Path and Source across users.
func (s shareBackend) GetBySourcePath(path, source string) ([]*share.Link, error) {
var v []*share.Link
err := s.db.Select(q.Eq("Path", path), q.Eq("Source", source)).Find(&v)
if err == storm.ErrNotFound {
return nil, errors.ErrNotExist
}
return v, err
}
func (s shareBackend) Gets(path, sourcePath string, id uint) ([]*share.Link, error) {
var v []*share.Link
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", sourcePath), q.Eq("UserID", id)).Find(&v)
@@ -60,7 +70,7 @@ func (s shareBackend) Gets(path, sourcePath string, id uint) ([]*share.Link, err
// through and filter out expired share
for i := range v {
if v[i].Expire < time.Now().Unix() && v[i].Expire != 0 {
err = s.Delete(v[i].PasswordHash)
err = s.Delete(v[i].Hash)
if err != nil {
logger.Errorf("expired share could not be deleted: %v", err.Error())
}
+15
View File
@@ -111,6 +111,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
// NEW PUBLIC ROUTES - All publicly accessible endpoints
// Share management routes (require permission but are publicly accessible)
publicRoutes.HandleFunc("GET /shares", withPermShare(shareListHandler))
publicRoutes.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler))
publicRoutes.HandleFunc("GET /share", withPermShare(shareGetHandler))
publicRoutes.HandleFunc("POST /share", withPermShare(sharePostHandler))
publicRoutes.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
@@ -279,6 +280,20 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
<-ctx.Done()
logger.Info("Shutting down HTTP server...")
// Persist in-memory state before shutting down the HTTP server
if store != nil {
if store.Share != nil {
if err := store.Share.Flush(); err != nil {
logger.Errorf("Failed to flush share storage: %v", err)
}
}
if store.Access != nil {
if err := store.Access.Flush(); err != nil {
logger.Errorf("Failed to flush access storage: %v", err)
}
}
}
// Graceful shutdown with a timeout - 30 seconds, in case downloads are happening
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
+8 -12
View File
@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"runtime"
"slices"
"strings"
@@ -47,12 +46,7 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
func withHashFileHelper(fn handleFunc) handleFunc {
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
hash := r.URL.Query().Get("hash")
encodedPath := r.URL.Query().Get("path")
// Decode the URL-encoded path
path, err := url.QueryUnescape(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
path := r.URL.Query().Get("path")
// Get the file link by hash
link, err := store.Share.GetByHash(hash)
if err != nil {
@@ -79,10 +73,13 @@ func withHashFileHelper(fn handleFunc) handleFunc {
if !ok {
return http.StatusNotFound, fmt.Errorf("source not found")
}
if source.Config.Private {
return http.StatusForbidden, fmt.Errorf("the target source is private")
}
// Get file information with options
file, err := FileInfoFasterFunc(iteminfo.FileOptions{
Path: utils.JoinPathAsUnix(link.Path, path),
Source: source.Name,
Source: link.Source,
Modify: false,
Expand: true,
Content: r.URL.Query().Get("content") == "true",
@@ -165,7 +162,7 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
// Call the handler function without user context
return fn(w, r, data)
}
return status, fmt.Errorf("could not authenticate share request")
return status, fmt.Errorf("could not authenticate request")
}
}
@@ -267,11 +264,10 @@ func withUserHelper(fn handleFunc) handleFunc {
// Check if the token is about to expire and send a header to renew it
if tk.Expires < time.Now().Add(time.Hour).Unix() {
w.Header().Add("X-Renew-Token", "true")
}
// Retrieve the user from the store and store it in the context
} // Retrieve the user from the store and store it in the context
data.user, err = store.Users.Get(tk.BelongsTo)
if err != nil {
logger.Errorf("Failed to get user with ID %v: %v", tk.BelongsTo, err)
return http.StatusInternalServerError, err
}
setUserInResponseWriter(w, data.user)
+10 -4
View File
@@ -150,8 +150,10 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
{
name: "Public share, no auth required",
share: &share.Link{
Hash: "public_hash",
Source: "/srv",
Hash: "public_hash",
CommonShare: share.CommonShare{
Source: "/srv",
},
},
expectedStatusCode: 0, // zero means 200 on helpers
},
@@ -162,7 +164,9 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
UserID: 1,
PasswordHash: passwordBcrypt,
Token: "some_random_token",
Source: "/srv",
CommonShare: share.CommonShare{
Source: "/srv",
},
},
extraHeaders: map[string]string{
"X-SHARE-PASSWORD": "password",
@@ -186,7 +190,9 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
UserID: 1,
PasswordHash: passwordBcrypt,
Token: "123",
Source: "/srv",
CommonShare: share.CommonShare{
Source: "/srv",
},
},
token: "123",
expectedStatusCode: 0, // zero means 200 on helpers
+7 -7
View File
@@ -292,15 +292,15 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
if config.Auth.Methods.OidcAuth.AdminGroup == "" {
isAdmin = config.UserDefaults.Permissions.Admin
}
newUser := users.User{
LoginMethod: users.LoginMethodOidc,
Username: username,
Permissions: settings.Config.UserDefaults.Permissions,
}
user = &users.User{}
settings.ApplyUserDefaults(user)
user.Username = username
user.LoginMethod = users.LoginMethodOidc
user.Permissions = settings.Config.UserDefaults.Permissions
if isAdmin {
newUser.Permissions.Admin = true
user.Permissions.Admin = true
}
err = storage.CreateUser(newUser)
err = storage.CreateUser(*user)
if err != nil {
return http.StatusInternalServerError, err
}
+3 -6
View File
@@ -19,7 +19,7 @@ import (
// @Tags Resources
// @Accept json
// @Produce json
// @Param files query string true "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)"
// @Param files query string false "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2"
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
@@ -29,17 +29,14 @@ import (
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /public/dl [get]
func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if d.share.Downloads > d.share.DownloadsLimit {
if d.share.DownloadsLimit > 0 && d.share.Downloads >= d.share.DownloadsLimit {
return http.StatusForbidden, fmt.Errorf("share downloads limit reached")
}
d.share.Mu.Lock()
d.share.Downloads++
d.share.Mu.Unlock()
encodedFiles := r.URL.Query().Get("files")
if encodedFiles == "" {
return http.StatusBadRequest, fmt.Errorf("no file list provided, should be in the format '/path1||/path2||/path3'")
}
// Decode the URL-encoded path
f, err := url.QueryUnescape(encodedFiles)
if err != nil {
+1 -1
View File
@@ -568,7 +568,7 @@ func patchAction(ctx context.Context, params patchActionParams) error {
// delete thumbnails
preview.DelThumbs(ctx, fileInfo)
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst)
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share)
default:
return fmt.Errorf("unsupported action %s: %w", params.action, errors.ErrInvalidRequestParams)
}
+207 -45
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
@@ -18,6 +17,7 @@ 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/indexing"
)
// shareListHandler returns a list of all share links.
@@ -37,17 +37,10 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
} else {
shares, err = store.Share.FindByUserID(d.user.ID)
}
if err != nil && err != errors.ErrNotExist {
return http.StatusInternalServerError, err
}
shares = utils.NonNilSlice(shares)
sort.Slice(shares, func(i, j int) bool {
if shares[i].UserID != shares[j].UserID {
return shares[i].UserID < shares[j].UserID
}
return shares[i].Expire < shares[j].Expire
})
return renderJSON(w, r, shares)
}
@@ -64,28 +57,28 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
// @Router /api/share [get]
func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
encodedPath := r.URL.Query().Get("path")
source := r.URL.Query().Get("source")
sourceName := r.URL.Query().Get("source")
// Decode the URL-encoded path
path, err := url.QueryUnescape(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
sourcePath, ok := config.Server.NameToSource[source]
sourceInfo, ok := config.Server.NameToSource[sourceName] // backend source is path
if !ok {
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", source)
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", sourceName)
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, sourceName)
if err != nil {
return http.StatusForbidden, err
}
scopePath := utils.JoinPathAsUnix(userscope, path)
s, err := store.Share.Gets(scopePath, sourcePath.Path, d.user.ID)
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.Source = source
link.DownloadURL = getDownloadURL(r, link.Hash)
}
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
@@ -125,12 +118,11 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// @Tags Shares
// @Accept json
// @Produce json
// @Param path query string true "Source Path of the files to share"
// @Param source query string true "Source name of the files to share"
// @Param body body share.CreateBody true "Share creation parameters"
// @Success 200 {object} share.Link "Created share link"
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/shares [post]
// @Router /api/share [post]
func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
var s *share.Link
var err error
@@ -192,49 +184,57 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
s.Expire = expire
s.PasswordHash = stringHash
s.Token = token
// Preserve immutable fields for updates. Path and Source should not change on edits.
// If the request attempts to provide empty values (or any values) for these,
// keep the existing ones from the stored share.
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)
}
source, ok := config.Server.NameToSource[body.Source]
if !ok {
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", body.Source)
}
if source.Config.Private {
return http.StatusForbidden, fmt.Errorf("the target source is private, sharing is not permitted")
}
// create a new share link
secure_hash, err := generateShortUUID()
if err != nil {
return http.StatusInternalServerError, err
}
// validate source path exists
idx := indexing.GetIndex(source.Name)
if idx == nil {
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", body.Source)
}
encodedPath := r.URL.Query().Get("path")
// Decode the URL-encoded path
path, err := url.QueryUnescape(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
sourceName := r.URL.Query().Get("source")
source, ok := config.Server.NameToSource[sourceName]
if !ok {
// try to find source by path
for _, s := range config.Server.Sources {
if s.Path == sourceName {
source = s
ok = true
break
}
// validate path exists as file or folder
_, exists := idx.GetReducedMetadata(body.Path, true) // true to check if it exists
if !exists {
// could be a file instead
_, exists := idx.GetReducedMetadata(utils.GetParentDirectoryPath(body.Path), true)
if !exists {
return http.StatusForbidden, fmt.Errorf("path not found: %s", body.Path)
}
}
if !ok {
return http.StatusForbidden, fmt.Errorf("source with name not found")
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source.Name)
if err != nil {
return http.StatusForbidden, err
}
scopePath := utils.JoinPathAsUnix(userscope, path)
scopePath := utils.JoinPathAsUnix(userscope, body.Path)
body.Path = scopePath
body.Source = source.Path // backend source is path
s = &share.Link{
Path: scopePath,
Source: source.Path, // path instead to persist accoss name change
Expire: expire,
UserID: d.user.ID,
Hash: secure_hash,
@@ -246,15 +246,177 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err := store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
// Overwrite the Source field with the source name from the query for each link
s.Source = sourceName
if body.Hash != "" {
return renderJSON(w, r, s)
}
return renderJSON(w, r, s)
}
// DirectDownloadResponse represents the response for direct download endpoint
type DirectDownloadResponse struct {
Status string `json:"status"`
Hash string `json:"hash"`
DownloadURL string `json:"url"`
}
// shareDirectDownloadHandler creates a direct download link for files only.
// @Summary Create direct download link
// @Description Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.
// @Tags Shares
// @Accept json
// @Produce json
// @Param path query string true "File path to create download link for"
// @Param source query string true "Source name for the file"
// @Param duration query string false "Duration in minutes for link validity (default: 60)"
// @Param count query string false "Maximum number of downloads allowed (default: unlimited)"
// @Param speed query string false "Download speed limit in kbps (default: unlimited)"
// @Success 201 {object} DirectDownloadResponse "Direct download link created"
// @Failure 400 {object} map[string]string "Bad request - invalid parameters or path is not a file"
// @Failure 403 {object} map[string]string "Forbidden - access denied"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /public/share/direct [get]
func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// Extract query parameters
encodedPath := r.URL.Query().Get("path")
source := r.URL.Query().Get("source")
duration := r.URL.Query().Get("duration")
downloadCountStr := r.URL.Query().Get("count")
downloadSpeedStr := r.URL.Query().Get("speed")
// Validate required parameters
if encodedPath == "" || source == "" {
return http.StatusBadRequest, fmt.Errorf("path and source are required")
}
// Decode the URL-encoded path
path, err := url.QueryUnescape(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
// Validate source exists
sourceInfo, ok := config.Server.NameToSource[source]
if !ok {
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", source)
}
// Get user scope for this source
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
return http.StatusForbidden, err
}
// Validate the path exists and is a file (not a folder)
idx := indexing.GetIndex(source)
if idx == nil {
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", source)
}
metadata, exists := idx.GetReducedMetadata(path, false)
if !exists {
return http.StatusBadRequest, fmt.Errorf("path is either not a file or not found: %s", path)
}
// Check if it's a file (not a directory)
if metadata.Type == "directory" {
return http.StatusBadRequest, fmt.Errorf("path must be a file, not a directory: %s", path)
}
// Set default duration to 60 minutes if not provided
if duration == "" {
duration = "60"
}
// Parse download count
var downloadCount int
if downloadCountStr != "" {
downloadCount, err = strconv.Atoi(downloadCountStr)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid downloadCount: %v", err)
}
}
// Parse download speed (in bytes per second)
var downloadSpeed int
if downloadSpeedStr != "" {
downloadSpeed, err = strconv.Atoi(downloadSpeedStr)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid downloadSpeed: %v", err)
}
}
// Calculate expiration time
durationNum, err := strconv.Atoi(duration)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid duration: %v", err)
}
expire := time.Now().Add(time.Minute * time.Duration(durationNum)).Unix()
// Generate secure hash for the share
secureHash, err := generateShortUUID()
if err != nil {
return http.StatusInternalServerError, err
}
// Create the scope path
scopePath := utils.JoinPathAsUnix(userscope, path)
// Check if an existing share already matches these parameters
existingShares, err := store.Share.Gets(scopePath, sourceInfo.Path, d.user.ID)
if err == nil && len(existingShares) > 0 {
// Look for a share that matches our parameters
for _, existing := range existingShares {
if existing.DownloadsLimit == downloadCount &&
existing.MaxBandwidth == downloadSpeed &&
existing.QuickDownload &&
(existing.Expire == 0 || existing.Expire >= expire) { // Existing expires later or never
response := DirectDownloadResponse{
Status: "201",
Hash: existing.Hash,
DownloadURL: getDownloadURL(r, existing.Hash),
}
return renderJSON(w, r, response)
}
}
}
// No matching existing share found, create a new one
shareLink := &share.Link{
Expire: expire,
UserID: d.user.ID,
Hash: secureHash,
CommonShare: share.CommonShare{
Path: scopePath,
Source: idx.Path,
DownloadsLimit: downloadCount,
MaxBandwidth: downloadSpeed,
QuickDownload: true, // Enable quick download for direct downloads
},
}
// Save the share
if err := store.Share.Save(shareLink); err != nil {
return http.StatusInternalServerError, err
}
// Return response
response := DirectDownloadResponse{
Status: "200",
Hash: secureHash,
DownloadURL: getDownloadURL(r, secureHash),
}
return renderJSON(w, r, response)
}
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)
} else {
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", getScheme(r), r.Host, config.Server.BaseURL, hash)
}
return downloadURL
}
func getSharePasswordHash(body share.CreateBody) (data []byte, statuscode int, err error) {
if body.Password == "" {
return nil, 0, nil
+6
View File
@@ -61,6 +61,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"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,
@@ -69,6 +70,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"disableThumbnails": false,
"viewMode": "list",
"disableFileViewer": false,
"disableShareCard": false,
"disableSidebar": false,
}
disableNavButtons := settings.Config.Frontend.DisableNavButtons
if d.share != nil {
@@ -81,6 +84,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
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
+266 -59
View File
@@ -1665,6 +1665,56 @@ const docTemplate = `{
}
}
}
},
"post": {
"description": "Creates a new share link with an optional expiration time and password protection.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create a share link",
"parameters": [
{
"description": "Share creation parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/share.CreateBody"
}
}
],
"responses": {
"200": {
"description": "Created share link",
"schema": {
"$ref": "#/definitions/share.Link"
}
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/shares": {
@@ -1701,61 +1751,6 @@ const docTemplate = `{
}
}
},
"post": {
"description": "Creates a new share link with an optional expiration time and password protection.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create a share link",
"parameters": [
{
"type": "string",
"description": "Source Path of the files to share",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name of the files to share",
"name": "source",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Created share link",
"schema": {
"$ref": "#/definitions/share.Link"
}
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes a share link specified by its hash.",
"consumes": [
@@ -2135,10 +2130,9 @@ const docTemplate = `{
"parameters": [
{
"type": "string",
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
"description": "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2",
"name": "files",
"in": "query",
"required": true
"in": "query"
},
{
"type": "boolean",
@@ -2198,6 +2192,90 @@ const docTemplate = `{
}
}
}
},
"/public/share/direct": {
"get": {
"description": "Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create direct download link",
"parameters": [
{
"type": "string",
"description": "File path to create download link for",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name for the file",
"name": "source",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Duration in minutes for link validity (default: 60)",
"name": "duration",
"in": "query"
},
{
"type": "string",
"description": "Maximum number of downloads allowed (default: unlimited)",
"name": "count",
"in": "query"
},
{
"type": "string",
"description": "Download speed limit in kbps (default: unlimited)",
"name": "speed",
"in": "query"
}
],
"responses": {
"201": {
"description": "Direct download link created",
"schema": {
"$ref": "#/definitions/http.DirectDownloadResponse"
}
},
"400": {
"description": "Bad request - invalid parameters or path is not a file",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden - access denied",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@@ -2221,6 +2299,20 @@ const docTemplate = `{
}
}
},
"http.DirectDownloadResponse": {
"type": "object",
"properties": {
"hash": {
"type": "string"
},
"status": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"http.GroupListResponse": {
"type": "object",
"properties": {
@@ -2897,6 +2989,10 @@ const docTemplate = `{
"description": "disable the indexing of this source",
"type": "boolean"
},
"disabled": {
"description": "disable the source, this is useful so you don't need to remove it from the config file",
"type": "boolean"
},
"exclude": {
"description": "exclude files and folders from indexing, if include is not set",
"allOf": [
@@ -2914,7 +3010,7 @@ const docTemplate = `{
]
},
"indexingIntervalMinutes": {
"description": "optional manual overide interval in seconds to re-index the source",
"description": "optional manual overide interval in minutes to re-index the source",
"type": "integer"
},
"maxWatchers": {
@@ -2927,6 +3023,10 @@ const docTemplate = `{
"items": {
"type": "string"
}
},
"private": {
"description": "designate as source as private -- currently just means no sharing permitted.",
"type": "boolean"
}
}
},
@@ -3073,6 +3173,97 @@ const docTemplate = `{
}
}
},
"share.CreateBody": {
"type": "object",
"properties": {
"allowUpload": {
"description": "AllowEdit bool ` + "`" + `json:\"allowEdit,omitempty\"` + "`" + `",
"type": "boolean"
},
"allowedUsernames": {
"type": "array",
"items": {
"type": "string"
}
},
"banner": {
"type": "string"
},
"description": {
"type": "string"
},
"disableAnonymous": {
"type": "boolean"
},
"disableFileViewer": {
"type": "boolean"
},
"disableShareCard": {
"type": "boolean"
},
"disableSidebar": {
"type": "boolean"
},
"disableThumbnails": {
"type": "boolean"
},
"downloadURL": {
"type": "string"
},
"downloadsLimit": {
"type": "integer"
},
"expires": {
"type": "string"
},
"favicon": {
"type": "string"
},
"hash": {
"type": "string"
},
"hideNavButtons": {
"type": "boolean"
},
"keepAfterExpiration": {
"type": "boolean"
},
"maxBandwidth": {
"type": "integer"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"quickDownload": {
"type": "boolean"
},
"shareTheme": {
"type": "string"
},
"source": {
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"themeColor": {
"type": "string"
},
"title": {
"type": "string"
},
"unit": {
"type": "string"
},
"username": {
"type": "string"
},
"viewMode": {
"type": "string"
}
}
},
"share.Link": {
"type": "object",
"properties": {
@@ -3098,9 +3289,21 @@ const docTemplate = `{
"disableFileViewer": {
"type": "boolean"
},
"disableShareCard": {
"type": "boolean"
},
"disableSidebar": {
"type": "boolean"
},
"disableThumbnails": {
"type": "boolean"
},
"downloadURL": {
"type": "string"
},
"downloads": {
"type": "integer"
},
"downloadsLimit": {
"type": "integer"
},
@@ -3135,6 +3338,7 @@ const docTemplate = `{
"type": "string"
},
"source": {
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"themeColor": {
@@ -3150,6 +3354,9 @@ const docTemplate = `{
"userID": {
"type": "integer"
},
"username": {
"type": "string"
},
"viewMode": {
"type": "string"
}
+266 -59
View File
@@ -1654,6 +1654,56 @@
}
}
}
},
"post": {
"description": "Creates a new share link with an optional expiration time and password protection.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create a share link",
"parameters": [
{
"description": "Share creation parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/share.CreateBody"
}
}
],
"responses": {
"200": {
"description": "Created share link",
"schema": {
"$ref": "#/definitions/share.Link"
}
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/shares": {
@@ -1690,61 +1740,6 @@
}
}
},
"post": {
"description": "Creates a new share link with an optional expiration time and password protection.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create a share link",
"parameters": [
{
"type": "string",
"description": "Source Path of the files to share",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name of the files to share",
"name": "source",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Created share link",
"schema": {
"$ref": "#/definitions/share.Link"
}
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Deletes a share link specified by its hash.",
"consumes": [
@@ -2124,10 +2119,9 @@
"parameters": [
{
"type": "string",
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
"description": "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2",
"name": "files",
"in": "query",
"required": true
"in": "query"
},
{
"type": "boolean",
@@ -2187,6 +2181,90 @@
}
}
}
},
"/public/share/direct": {
"get": {
"description": "Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Create direct download link",
"parameters": [
{
"type": "string",
"description": "File path to create download link for",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name for the file",
"name": "source",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Duration in minutes for link validity (default: 60)",
"name": "duration",
"in": "query"
},
{
"type": "string",
"description": "Maximum number of downloads allowed (default: unlimited)",
"name": "count",
"in": "query"
},
{
"type": "string",
"description": "Download speed limit in kbps (default: unlimited)",
"name": "speed",
"in": "query"
}
],
"responses": {
"201": {
"description": "Direct download link created",
"schema": {
"$ref": "#/definitions/http.DirectDownloadResponse"
}
},
"400": {
"description": "Bad request - invalid parameters or path is not a file",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden - access denied",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@@ -2210,6 +2288,20 @@
}
}
},
"http.DirectDownloadResponse": {
"type": "object",
"properties": {
"hash": {
"type": "string"
},
"status": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"http.GroupListResponse": {
"type": "object",
"properties": {
@@ -2886,6 +2978,10 @@
"description": "disable the indexing of this source",
"type": "boolean"
},
"disabled": {
"description": "disable the source, this is useful so you don't need to remove it from the config file",
"type": "boolean"
},
"exclude": {
"description": "exclude files and folders from indexing, if include is not set",
"allOf": [
@@ -2903,7 +2999,7 @@
]
},
"indexingIntervalMinutes": {
"description": "optional manual overide interval in seconds to re-index the source",
"description": "optional manual overide interval in minutes to re-index the source",
"type": "integer"
},
"maxWatchers": {
@@ -2916,6 +3012,10 @@
"items": {
"type": "string"
}
},
"private": {
"description": "designate as source as private -- currently just means no sharing permitted.",
"type": "boolean"
}
}
},
@@ -3062,6 +3162,97 @@
}
}
},
"share.CreateBody": {
"type": "object",
"properties": {
"allowUpload": {
"description": "AllowEdit bool `json:\"allowEdit,omitempty\"`",
"type": "boolean"
},
"allowedUsernames": {
"type": "array",
"items": {
"type": "string"
}
},
"banner": {
"type": "string"
},
"description": {
"type": "string"
},
"disableAnonymous": {
"type": "boolean"
},
"disableFileViewer": {
"type": "boolean"
},
"disableShareCard": {
"type": "boolean"
},
"disableSidebar": {
"type": "boolean"
},
"disableThumbnails": {
"type": "boolean"
},
"downloadURL": {
"type": "string"
},
"downloadsLimit": {
"type": "integer"
},
"expires": {
"type": "string"
},
"favicon": {
"type": "string"
},
"hash": {
"type": "string"
},
"hideNavButtons": {
"type": "boolean"
},
"keepAfterExpiration": {
"type": "boolean"
},
"maxBandwidth": {
"type": "integer"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"quickDownload": {
"type": "boolean"
},
"shareTheme": {
"type": "string"
},
"source": {
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"themeColor": {
"type": "string"
},
"title": {
"type": "string"
},
"unit": {
"type": "string"
},
"username": {
"type": "string"
},
"viewMode": {
"type": "string"
}
}
},
"share.Link": {
"type": "object",
"properties": {
@@ -3087,9 +3278,21 @@
"disableFileViewer": {
"type": "boolean"
},
"disableShareCard": {
"type": "boolean"
},
"disableSidebar": {
"type": "boolean"
},
"disableThumbnails": {
"type": "boolean"
},
"downloadURL": {
"type": "string"
},
"downloads": {
"type": "integer"
},
"downloadsLimit": {
"type": "integer"
},
@@ -3124,6 +3327,7 @@
"type": "string"
},
"source": {
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"themeColor": {
@@ -3139,6 +3343,9 @@
"userID": {
"type": "integer"
},
"username": {
"type": "string"
},
"viewMode": {
"type": "string"
}
+184 -42
View File
@@ -12,6 +12,15 @@ definitions:
name:
type: string
type: object
http.DirectDownloadResponse:
properties:
hash:
type: string
status:
type: string
url:
type: string
type: object
http.GroupListResponse:
properties:
groups:
@@ -509,6 +518,10 @@ definitions:
disableIndexing:
description: disable the indexing of this source
type: boolean
disabled:
description: disable the source, this is useful so you don't need to remove
it from the config file
type: boolean
exclude:
allOf:
- $ref: '#/definitions/settings.ExcludeIndexFilter'
@@ -518,7 +531,7 @@ definitions:
- $ref: '#/definitions/settings.IncludeIndexFilter'
description: include files and folders from indexing, if exclude is not set
indexingIntervalMinutes:
description: optional manual overide interval in seconds to re-index the source
description: optional manual overide interval in minutes to re-index the source
type: integer
maxWatchers:
description: number of concurrent watchers to use for this source, currently
@@ -530,6 +543,10 @@ definitions:
items:
type: string
type: array
private:
description: designate as source as private -- currently just means no sharing
permitted.
type: boolean
type: object
settings.StylingConfig:
properties:
@@ -643,6 +660,67 @@ definitions:
description: 'view mode to use: eg. normal, list, grid, or compact'
type: string
type: object
share.CreateBody:
properties:
allowUpload:
description: AllowEdit bool `json:"allowEdit,omitempty"`
type: boolean
allowedUsernames:
items:
type: string
type: array
banner:
type: string
description:
type: string
disableAnonymous:
type: boolean
disableFileViewer:
type: boolean
disableShareCard:
type: boolean
disableSidebar:
type: boolean
disableThumbnails:
type: boolean
downloadURL:
type: string
downloadsLimit:
type: integer
expires:
type: string
favicon:
type: string
hash:
type: string
hideNavButtons:
type: boolean
keepAfterExpiration:
type: boolean
maxBandwidth:
type: integer
password:
type: string
path:
type: string
quickDownload:
type: boolean
shareTheme:
type: string
source:
description: backend source is path to maintain between name changes
type: string
themeColor:
type: string
title:
type: string
unit:
type: string
username:
type: string
viewMode:
type: string
type: object
share.Link:
properties:
allowUpload:
@@ -660,8 +738,16 @@ definitions:
type: boolean
disableFileViewer:
type: boolean
disableShareCard:
type: boolean
disableSidebar:
type: boolean
disableThumbnails:
type: boolean
downloadURL:
type: string
downloads:
type: integer
downloadsLimit:
type: integer
expire:
@@ -685,6 +771,7 @@ definitions:
shareTheme:
type: string
source:
description: backend source is path to maintain between name changes
type: string
themeColor:
type: string
@@ -698,6 +785,8 @@ definitions:
type: string
userID:
type: integer
username:
type: string
viewMode:
type: string
type: object
@@ -2023,6 +2112,40 @@ paths:
summary: Get share links by path
tags:
- Shares
post:
consumes:
- application/json
description: Creates a new share link with an optional expiration time and password
protection.
parameters:
- description: Share creation parameters
in: body
name: body
required: true
schema:
$ref: '#/definitions/share.CreateBody'
produces:
- application/json
responses:
"200":
description: Created share link
schema:
$ref: '#/definitions/share.Link'
"400":
description: Bad request - failed to decode body
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Create a share link
tags:
- Shares
/api/shares:
delete:
consumes:
@@ -2077,44 +2200,6 @@ paths:
summary: List share links
tags:
- Shares
post:
consumes:
- application/json
description: Creates a new share link with an optional expiration time and password
protection.
parameters:
- description: Source Path of the files to share
in: query
name: path
required: true
type: string
- description: Source name of the files to share
in: query
name: source
required: true
type: string
produces:
- application/json
responses:
"200":
description: Created share link
schema:
$ref: '#/definitions/share.Link'
"400":
description: Bad request - failed to decode body
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Create a share link
tags:
- Shares
/api/signup:
post:
consumes:
@@ -2332,11 +2417,10 @@ paths:
description: Returns the raw content of a file, multiple files, or a directory.
Supports downloading files as archives in various formats.
parameters:
- description: a list of files in the following format 'filename' and separated
by '||' with additional items in the list. (required)
- description: if specified, only the files in the list will be downloaded.
eg. files=/file1||/folder/file2
in: query
name: files
required: true
type: string
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
to 'attachment'.
@@ -2382,4 +2466,62 @@ paths:
summary: Get raw content of a file, multiple files, or directory
tags:
- Resources
/public/share/direct:
get:
consumes:
- application/json
description: Creates a direct download link for a specific file with configurable
duration, download count, and speed limits. If a share already exists with
matching parameters, the existing share will be reused.
parameters:
- description: File path to create download link for
in: query
name: path
required: true
type: string
- description: Source name for the file
in: query
name: source
required: true
type: string
- description: 'Duration in minutes for link validity (default: 60)'
in: query
name: duration
type: string
- description: 'Maximum number of downloads allowed (default: unlimited)'
in: query
name: count
type: string
- description: 'Download speed limit in kbps (default: unlimited)'
in: query
name: speed
type: string
produces:
- application/json
responses:
"201":
description: Direct download link created
schema:
$ref: '#/definitions/http.DirectDownloadResponse'
"400":
description: Bad request - invalid parameters or path is not a file
schema:
additionalProperties:
type: string
type: object
"403":
description: Forbidden - access denied
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Create direct download link
tags:
- Shares
swagger: "2.0"
+3 -1
View File
@@ -21,7 +21,9 @@ server:
- path: "/relative/or/absolute/path" # file system path. (Can be relative) validate:required
name: "backend" # display name
config:
indexingIntervalMinutes: 0 # optional manual overide interval in seconds to re-index the source
private: false # designate as source as private -- currently just means no sharing permitted.
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
disableIndexing: false # disable the indexing of this source
maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported
neverWatchPaths: []
+2 -5
View File
@@ -132,14 +132,11 @@ export async function remove(hash) {
// Create a new share
/**
* @param {string} path
* @param {string} source
* @param {Record<string, any>} bodyObj
* @returns {Promise<Share>}
*/
export async function create(path, source, bodyObj = {}) {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("public/share", params);
export async function create(bodyObj = {}) {
const apiPath = getApiPath("public/share");
return fetchJSON(apiPath, {
method: "POST",
body: JSON.stringify(bodyObj || {}),
+1 -1
View File
@@ -52,7 +52,7 @@ export default {
return getters.multibuttonState();
},
stickSidebar() {
return state.stickSidebar;
return state.user?.stickySidebar;
},
currentView() {
return getters.currentView();
+1
View File
@@ -7,6 +7,7 @@
<button
v-for="(btn, index) in buttons"
:key="index"
class="clickable"
:class="{ active: activeButton === index }"
@click="setActiveButton(index, btn.value)"
>
+41 -31
View File
@@ -17,47 +17,37 @@
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} {{ $t("prompts.selected") }} </span>
</div>
<action
v-if="!showCreate && !isSearchActive && userPerms.modify"
v-if="!showCreate && !isSearchActive && userPerms.modify && !isShare"
icon="add"
:label="$t('buttons.new')"
@action="startShowCreate"
/>
<action
v-if="showCreate && !isSearchActive && userPerms.modify"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="!showCreate && selectedCount == 1"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="showSelectMultiple"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="(!showCreate && selectedCount > 0)"
icon="file_download"
@@ -69,13 +59,13 @@
v-if="selectedCount <= 1 && showShare"
icon="share"
:label="$t('buttons.share')"
show="share"
@action="showShareHover"
/>
<action
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
@action="showRenameHover"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
@@ -90,13 +80,7 @@
show="move"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="file_upload"
:label="$t('buttons.upload')"
@action="showUpload"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
v-if="showDelete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
@@ -107,6 +91,12 @@
:label="$t('access.rules')"
@action="showAccessHover"
/>
<action
v-if="showSelectMultiple"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
</div>
</transition>
<transition
@@ -165,6 +155,9 @@ export default {
},
},
computed: {
isShare() {
return getters.isShare();
},
showSelectMultiple() {
if (this.isMultiple || this.isSearchActive) {
return false;
@@ -194,11 +187,12 @@ export default {
},
showDelete() {
const cv = getters.currentView();
if (getters.isShare()) {
if (getters.isShare() || !state.user?.permissions?.modify) {
// TODO: add support for deleting shared files
return false;
}
return state.user?.permissions?.modify && (cv == "preview" || cv == "onlyOfficeEditor" || cv == "markdownViewer" || cv == "epubViewer" || cv == "docViewer" || cv == "editor");
const showDelete = cv != "settings" && !this.showCreate && this.selectedCount > 0 && !this.isSearchActive;
return showDelete;
},
isPreview() {
const cv = getters.currentView();
@@ -225,13 +219,10 @@ export default {
return state.user?.permissions?.admin && this.showCreate;
},
showShare() {
return (
state.user?.permissions &&
state.user?.permissions.share &&
state.user.username != "anonymous" &&
!getters.isShare() &&
!this.isSearchActive
);
if (getters.isShare()) {
return false;
}
return state.user?.permissions?.share;
},
showContext() {
return getters.currentPromptName() == "ContextMenu";
@@ -368,6 +359,9 @@ export default {
}, 300);
},
startShowCreate() {
if (getters.isShare()) {
return;
}
this.showCreate = true;
},
uploadFunc() {
@@ -376,6 +370,22 @@ export default {
showHover(value) {
return mutations.showHover(value);
},
showShareHover() {
mutations.showHover({
name: "share",
props: {
item: getters.selectedCount() == 1 ? getters.getFirstSelected() : state.req
},
});
},
showRenameHover() {
mutations.showHover({
name: "rename",
props: {
item: getters.selectedCount() == 1 ? getters.getFirstSelected() : state.req
},
});
},
setPositions() {
const contextProps = getters.currentPrompt().props;
this.posX = contextProps.posX;
@@ -383,7 +393,7 @@ export default {
},
initializeCreateState() {
// Only set initial showCreate state, don't override user choices
if (state.selected.length > 0) {
if (state.selected.length > 0 || getters.isShare()) {
this.showCreate = false;
} else {
this.showCreate = true;
+1 -1
View File
@@ -122,7 +122,7 @@
<li
v-for="(s, k) in results"
:key="k"
class="search-entry"
class="search-entry clickable"
:class="{ active: activeStates[k] }"
:aria-label="baseName(s.path)"
>
+5 -1
View File
@@ -19,7 +19,7 @@
/>
</span>
<span v-else>
<i :class="[classes, { active: active }]" class="icon"> {{ materialIcon }} </i>
<i :class="[classes, { active: active, clickable: clickable }]" class="icon"> {{ materialIcon }} </i>
</span>
</template>
@@ -56,6 +56,10 @@ export default {
type: String,
default: "",
},
clickable: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -1,10 +1,8 @@
<template>
<a
:href="getUrl()"
class="item listing-item clickable no-select"
:class="{
item: true,
'no-select': true,
'listing-item': true,
activebutton: isSelected,
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
'half-selected': isDraggedOver,
@@ -62,6 +60,7 @@
role="button"
aria-label="Download"
tabindex="0"
:clickable=true
/>
</a>
</template>
+2 -2
View File
@@ -5,7 +5,7 @@
<img :src="getShareBanner" />
</div>
<div v-if="shareOverrides.title" class="share__box__element">
<h2>{{ shareOverrides.title }}</h2>
<h3>{{ shareOverrides.title }}</h3>
</div>
<div v-if="shareOverrides.description" class="share__box__element">
<p>{{ shareOverrides.description }}</p>
@@ -22,7 +22,7 @@
</div>
<div class="share__box__element share__box__center">
<button class="button button--flat" @click="goToLink()"> {{ $t("buttons.download") }} </button>
<button class="button button--flat clickable" @click="goToLink()"> {{ $t("buttons.download") }} </button>
</div>
<div v-if="req.type" class="share__box__element share__box__center">
+1 -1
View File
@@ -16,7 +16,7 @@
</div>
<div v-else>
<p>{{ $t("prompts.source", { suffix: ":" }) }} {{ currentSource }}</p>
<div aria-label="access-path" class="searchContext" @click="startPathEdit" style="cursor: pointer;">
<div aria-label="access-path" class="searchContext clickable" @click="startPathEdit">
{{ $t("search.path") }} {{ currentPath }}
</div>
<!-- Add Form -->
+1 -1
View File
@@ -14,7 +14,7 @@
</div>
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ sourcePath.path }}</div>
<div aria-label="filelist-path" class="searchContext clickable">{{$t('search.path')}} {{ sourcePath.path }}</div>
<ul class="file-list">
<li
@click="itemClick"
+2 -10
View File
@@ -50,16 +50,8 @@ export default {
try {
event.preventDefault();
if (this.name === "") return;
await filesApi.post(
state.req.source,
state.req.path + "/" + this.name,
"",
true
);
goToItem(
state.req.source,
state.req.path + "/" + encodeURIComponent(this.name)
);
await filesApi.post(state.req.source,state.req.path + "/" + this.name, "", true );
goToItem(state.req.source, state.req.path + "/" + this.name);
} catch (error) {
notify.showError(error);
}
+19 -55
View File
@@ -4,7 +4,7 @@
</div>
<div class="card-content">
<p>{{ $t("prompts.renameMessage", { filename: oldName() }) }}</p>
<p>{{ $t("prompts.renameMessage", { filename: oldName }) }}</p>
<input class="input" :class="{ 'form-invalid': !validateFileName(name) }" v-focus type="text"
@keyup.enter="submit" v-model.trim="name" />
<p v-if="!validateFileName(name) && name.length > 0" class="validation-error">
@@ -25,40 +25,35 @@
</template>
<script>
import { filesApi } from "@/api";
import { state, getters, mutations } from "@/store";
import { mutations } from "@/store";
import { notify } from "@/notify";
export default {
name: "rename",
props: {
item: {
type: Object,
required: true,
default: () => ({ source: "", path: "", name: "" })
}
},
data() {
return {
name: "",
name: this.item?.name || "",
};
},
created() {
this.name = this.oldName();
},
computed: {
req() {
return state.req;
},
selected() {
return state.selected;
},
selectedCount() {
return state.selectedCount;
},
isListing() {
return getters.isListing();
},
closeHovers() {
return mutations.closeHovers;
},
currentPrompt() {
return getters.currentPrompt();
oldName() {
return this.item?.name || "";
},
},
methods: {
/**
* @param {string} value
*/
validateFileName(value) {
if (value === "") {
return false;
@@ -67,25 +62,6 @@ export default {
const forbiddenChars = /[/\\]/;
return !forbiddenChars.test(value);
},
cancel() {
mutations.closeHovers();
},
oldName() {
// Check if this is being called from upload context with props
if (this.currentPrompt && this.currentPrompt.props && this.currentPrompt.props.folderName) {
return this.currentPrompt.props.folderName;
}
if (!this.isListing) {
return state.req.name;
}
if (getters.selectedCount() === 0 || getters.selectedCount() > 1) {
return "";
}
return state.req.items[this.selected[0]].name;
},
async submit() {
// Validate before submitting
if (!this.validateFileName(this.name) || this.name.length === 0) {
@@ -94,27 +70,15 @@ export default {
}
try {
// Check if this is being called from upload context with custom confirm handler
if (this.currentPrompt && this.currentPrompt.confirm) {
// This is for upload rename - call the custom confirm handler
this.currentPrompt.confirm(this.name);
return;
}
// Default file rename operation
const items = [{
from: state.req.path + "/" + state.req.items[this.selected[0]].name,
fromSource: state.req.source,
to: state.req.path + "/" + this.name,
toSource: state.req.source,
from: this.item.path,
fromSource: this.item.source,
to: this.item.path.replace(/[^/]+$/, this.name),
toSource: this.item.source,
}];
await filesApi.moveCopy(items, "move");
mutations.closeHovers();
if (!this.isListing) {
this.$router.push({ path: newLink });
return;
}
} catch (error) {
notify.showError(error);
}
+39 -86
View File
@@ -3,7 +3,7 @@
<h2>{{ $t("buttons.share") }}</h2>
</div>
<div class="card-content">
<div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ subpath }}</div>
<div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ item.path }}</div>
<p> {{ $t('share.notice') }} </p>
<div v-if="listing">
@@ -75,13 +75,12 @@
</i>
</p>
<input class="input" type="password" autocomplete="new-password" v-model.trim="password" />
<div class="settings-items">
<div class="settings-items" style="margin-top: 0.5em;">
<!--
<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="disablingFileViewer" :name="'Disable File Viewer'" />
<ToggleSwitch
class="item"
v-model="quickDownload"
@@ -136,6 +135,8 @@
<ToggleSwitch class="item" v-model="keepAfterExpiration" :name="$t('share.keepAfterExpiration')" :description="$t('share.keepAfterExpirationDescription')" />
<ToggleSwitch class="item" v-model="disableThumbnails" :name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
<ToggleSwitch class="item" v-model="disableNavButtons" :name="$t('share.hideNavButtons')" :description="$t('share.hideNavButtonsDescription')" />
<ToggleSwitch class="item" v-model="disableShareCard" :name="$t('share.disableShareCard')" :description="$t('share.disableShareCardDescription')" />
<ToggleSwitch class="item" v-model="disableSidebar" :name="$t('share.disableSidebar')" :description="$t('share.disableSidebarDescription')" />
</div>
<p>
@@ -257,29 +258,6 @@ import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import { userSelectableThemes } from "@/utils/constants";
//import ViewMode from "@/components/settings/ViewMode.vue";
/**
* @typedef {import('@/api/public').Share} Share
*/
/**
* @typedef {object} FilebrowserFile
* @property {string} name
* @property {string} path
* @property {string} source
* @property {boolean} isDir
* @property {string} type
*/
/**
* @typedef {object} FilebrowserRequest
* @property {FilebrowserFile[]} items
* @property {number} numDirs
* @property {number} numFiles
* @property {{by: string, asc: boolean}} sorting
* @property {string} path
* @property {string} source
*/
export default {
name: "share",
components: {
@@ -295,6 +273,15 @@ export default {
type: Object,
default: () => ({}),
},
item: {
type: Object,
default: () => ({
path: "",
source: "",
isDir: false,
type: "",
}),
},
},
data() {
return {
@@ -304,8 +291,6 @@ export default {
links: [],
/** @type {Clipboard | null} */
clip: null,
subpath: "",
source: "",
password: "",
listing: true,
allowEdit: false,
@@ -327,6 +312,8 @@ export default {
showAdvanced: false,
quickDownload: false,
disableNavButtons: false,
disableShareCard: false,
disableSidebar: false,
//viewMode: "normal",
};
},
@@ -399,35 +386,20 @@ export default {
this.favicon = this.link.favicon || "";
this.quickDownload = this.link.quickDownload || false;
this.disableNavButtons = this.link.hideNavButtons || false;
this.viewMode = this.link.viewMode || "normal";
this.disableShareCard = this.link.disableShareCard || false;
this.disableSidebar = this.link.disableSidebar || false;
//this.viewMode = this.link.viewMode || "normal";
}
},
},
},
async beforeMount() {
if (this.isEditMode) {
this.subpath = this.link.path;
this.source = this.link.source;
return;
}
let path = this.req.path;
this.source = this.req.source;
if (state.isSearchActive) {
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
path = file.path;
this.source = file.source;
} else if (this.selectedCount === 1) {
const index = /** @type {number} */ (this.selected[0]);
const selected = this.req.items[index];
path = selected.path;
this.source = selected.source;
}
// double encode # to fix issue with # in path
// replace all # with %23
this.subpath = path.replace(/#/g, "%23");
try {
// get last element of the path
const links = await publicApi.get(this.subpath, this.source);
const links = await publicApi.get(this.item.path, this.item.source);
this.links = links;
} catch (err) {
notify.showError(err);
@@ -501,18 +473,23 @@ export default {
},
async submit() {
try {
if (!this.description) {
this.description = this.$t("share.descriptionDefault");
}
if (!this.title) {
this.title = this.$t("share.titleDefault", { title: this.item.name || "share" });
}
let isPermanent = !this.time || this.time === "0";
const payload = {
path: this.subpath,
sourceName: this.source,
source: this.source,
path: this.item.path,
source: this.item.source,
password: this.password,
expires: isPermanent ? "" : this.time.toString(),
unit: this.unit,
disableAnonymous: this.disableAnonymous,
allowUpload: this.allowUpload,
maxBandwidth: this.maxBandwidth,
downloadsLimit: this.downloadsLimit,
maxBandwidth: this.maxBandwidth ? parseInt(this.maxBandwidth) : 0,
downloadsLimit: this.downloadsLimit ? parseInt(this.downloadsLimit) : 0,
shareTheme: this.shareTheme,
disablingFileViewer: this.disablingFileViewer,
disableThumbnails: this.disableThumbnails,
@@ -526,34 +503,14 @@ export default {
favicon: this.favicon,
quickDownload: this.quickDownload,
hideNavButtons: this.disableNavButtons,
disableShareCard: this.disableShareCard,
disableSidebar: this.disableSidebar,
};
if (this.isEditMode) {
payload.hash = this.link.hash;
}
const res = await publicApi.create(payload.path, payload.source, {
password: payload.password,
expires: payload.expires,
unit: payload.unit,
disableAnonymous: payload.disableAnonymous,
allowUpload: payload.allowUpload,
maxBandwidth: Number(payload.maxBandwidth) || 0,
downloadsLimit: Number(payload.downloadsLimit) || 0,
shareTheme: payload.shareTheme,
disableFileViewer: payload.disablingFileViewer,
disableThumbnails: payload.disableThumbnails,
allowedUsernames: payload.allowedUsernames,
hash: payload.hash,
keepAfterExpiration: payload.keepAfterExpiration,
themeColor: payload.themeColor,
banner: payload.banner,
title: payload.title,
description: payload.description,
favicon: payload.favicon,
quickDownload: payload.quickDownload,
hideNavButtons: payload.hideNavButtons,
//viewMode: this.viewMode,
});
const res = await publicApi.create(payload);
if (!this.isEditMode) {
this.links.push(res);
@@ -601,21 +558,17 @@ export default {
return publicApi.getShareURL(share);
},
hasDownloadLink() {
if (state.isSearchActive) {
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
return file.type !== "directory";
}
const index = /** @type {number} */ (this.selected[0]);
return this.selected.length === 1 && !this.req.items[index].isDir;
// Check if we have a single selected item that can be downloaded
return !this.item?.isDir;
},
/**
* @param {Share} share
*/
buildDownloadLink(share) {
share.source = this.source;
share.path = "/";
const index = /** @type {number} */ (this.selected[0]);
return publicApi.getDownloadURL(share, [this.req.items[index].name]);
if (share.downloadURL) {
return share.downloadURL;
}
return publicApi.getDownloadURL(share, [this.item.name]);
},
sort() {
this.links = this.links.sort((a, b) => {
@@ -43,6 +43,7 @@
<div v-if="!disableQuickToggles" class="card-wrapper" @mouseleave="hideTooltip">
<div class="quick-toggles">
<div
class="clickable"
:class="{ active: user?.singleClick }"
@click="toggleClick"
@mouseenter="showTooltip($event, $t('index.toggleClick'))"
@@ -51,6 +52,7 @@
<i class="material-icons">ads_click</i>
</div>
<div
class="clickable"
:class="{ active: user?.darkMode }"
@click="toggleDarkMode"
@mouseenter="showTooltip($event, $t('index.toggleDark'))"
@@ -59,6 +61,7 @@
<i class="material-icons">dark_mode</i>
</div>
<div
class="clickable"
:class="{ active: isStickySidebar }"
@click="toggleSticky"
@mouseenter="showTooltip($event, $t('index.toggleSticky'))"
+1 -1
View File
@@ -54,7 +54,7 @@ export default {
<style>
.active-settings {
background: var(--primaryColor) !important;
color: var(--item-selected) !important;
color: white !important;
}
.settings-card {
@@ -2,6 +2,7 @@
<div class="share-sidebar">
<div class="share-sidebar__content">
<ShareInfo
v-if="!disableShareCard"
:hash="hash"
:token="token"
:sub-path="subPath"
@@ -12,6 +13,7 @@
<script>
import ShareInfo from "@/components/files/ShareInfo.vue";
import { shareOverrides } from "@/utils/constants";
export default {
name: "SidebarShare",
@@ -32,6 +34,11 @@ export default {
default: "/",
},
},
computed: {
disableShareCard() {
return shareOverrides.disableShareCard;
},
},
};
</script>
@@ -172,14 +172,6 @@ body.rtl .action {
flex-grow: 1;
}
.clickable {
cursor: pointer;
}
.clickable:hover {
box-shadow: 0 2px 2px #00000024, 0 1px 5px #0000001f, 0 3px 1px -2px #0003;
}
.release-banner {
background-color: var(--primarColor);
display: flex;
-1
View File
@@ -7,7 +7,6 @@
width: 100%;
background: var(--surfaceSecondary);
color: var(--textPrimary);
height: -webkit-fill-available;
}
.input:hover,
+1 -1
View File
@@ -240,7 +240,7 @@ body.rtl .card-title>*:first-child {
.card h3 {
font-size: 1em;
font-weight: 500;
margin: 2em 0 1em;
margin: 1em 0 1em;
}
.card code {
+9 -2
View File
@@ -125,6 +125,7 @@ body.rtl #listingView {
overflow: hidden;
justify-content: center;
padding: 0;
position: relative;
}
#listingView.gallery .item .gallery-div {
@@ -140,8 +141,10 @@ body.rtl #listingView {
#listingView.gallery .item .text {
text-shadow: 0 0 2px white;
pointer-events: none;
font-weight: bold;
margin-top: -1em;
font-weight: 700;
position: absolute;
z-index: 1;
transform: translateY(6em);
}
#listingView.gallery .item .text .name {
@@ -162,6 +165,10 @@ body.rtl #listingView {
display: flex;
}
#listingView.list .item img,
#listingView.compact .item img {
min-width: unset;
}
#listingView.gallery .item img {
width: 100%;
+7 -7
View File
@@ -346,13 +346,14 @@ body.rtl .card.floating .card-content .file-list {
-webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
}
.item:hover,
.quick-toggles div:hover,
.button-group button:hover,
.search-entry:hover,
.clickable {
cursor: pointer;
}
.clickable:hover,
button:hover,
.searchContext:hover,
.item.drag-hover{
.action:hover,
.listing-item.drag-hover {
box-shadow: inset 0 -3em 3em rgba(217, 217, 217, 0.211), 0 0 0 2px var(--alt-background) !important;
/* Adjust shadow values as needed */
transform: scale(1.02);
@@ -400,5 +401,4 @@ button:hover,
.form-dropdown {
width: unset !important;
height: -webkit-fill-available;
}
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "مشاركة الإعدادات",
"adminOptions": "خيارات المستخدم المسؤول",
"shareDuration": "تنتهي صلاحيتها",
"searchOptions": "خيارات البحث"
"searchOptions": "خيارات البحث",
"downloads": "التنزيلات"
},
"sidebar": {
"help": "مساعدة",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "عنوان URL أو المسار الذي يمكن للعميل الوصول إليه من قبل العميل - يتم قبول إما المسار المطلق أو مسار الفهرس: على سبيل المثال 'https://domain.com/favicon.png' أو '/path/to/favicon.png'",
"hideNavButtons": "إخفاء أزرار التنقل",
"hideNavButtonsDescription": "قم بإخفاء أزرار التنقل على شريط التنقل في المشاركة لإضفاء مظهر بسيط.",
"viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة."
"viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "تم إرسال مشاركة إليك لعرضها أو تنزيلها.",
"disableShareCard": "تعطيل بطاقة المشاركة المعطلة",
"disableShareCardDescription": "قم بتعطيل بطاقة المشاركة على الصفحة المشتركة في الشريط الجانبي أو في أعلى الصفحة على الهاتف المحمول.",
"disableSidebar": "تعطيل الشريط الجانبي",
"disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة."
},
"api": {
"title": "مفاتيح واجهة برمجة التطبيقات (API):",
@@ -391,7 +398,6 @@
"toggleClick": "تبديل نقرة واحدة",
"toggleDark": "تبديل الوضع الداكن",
"toggleSticky": "تبديل الوضع اللاصق",
"of": "من",
"used": "مستعمل",
"noSources": "لا توجد مصادر متاحة لهذا المستخدم. إما أن تقوم بإضافة مصدر لهذا المستخدم أو تكوين مصدر مع defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "المجلدات",
"exit": "الخروج",
"permanent": "دائم",
"hash": "تجزئة"
"hash": "تجزئة",
"of": "من"
},
"access": {
"rules": "قواعد الوصول",
+11 -4
View File
@@ -158,7 +158,13 @@
"shareFaviconDescription": "Adresa URL nebo cesta k faviconě přístupná klientovi -- akceptována je buď absolutní, nebo indexová cesta: např. \"https://domain.com/favicon.png\" nebo \"/path/to/favicon.png\".",
"hideNavButtons": "Skrytí navigačních tlačítek",
"hideNavButtonsDescription": "Skryjte navigační tlačítka na panelu navigace ve sdíleném okně, abyste vytvořili minimalistický vzhled.",
"viewModeDescription": "Výchozí rozložení sdílené stránky."
"viewModeDescription": "Výchozí rozložení sdílené stránky.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Byl vám zaslán podíl, který si můžete prohlédnout nebo stáhnout.",
"disableShareCard": "Zakázat sdílenou kartu",
"disableShareCardDescription": "Zakázat kartu sdílení na sdílené stránce v postranním panelu nebo v horní části stránky na mobilním telefonu.",
"disableSidebar": "Zakázat postranní panel",
"disableSidebarDescription": "Zakázat postranní panel na sdílené stránce."
},
"prompts": {
"copy": "Kopírovat",
@@ -379,7 +385,8 @@
"userUpdated": "Uživatel aktualizován!",
"username": "Uživatelské jméno",
"users": "Uživatelé",
"adminOptions": "Možnosti administrátora"
"adminOptions": "Možnosti administrátora",
"downloads": "Ke stažení na"
},
"sidebar": {
"help": "Nápověda",
@@ -442,7 +449,6 @@
"toggleClick": "Přepnout jedno kliknutí",
"toggleDark": "Přepnout tmavý režim",
"toggleSticky": "Přepnout sticky režim",
"of": "z",
"used": "použito",
"noSources": "Pro tohoto uživatele nejsou dostupné žádné zdroje. Přidejte zdroj pro uživatele nebo nastavte zdroj s defaultEnabled: true"
},
@@ -484,7 +490,8 @@
"folders": "Složky",
"exit": "Exit",
"permanent": "Permanentní",
"hash": "Hash"
"hash": "Hash",
"of": "z"
},
"access": {
"rules": "Pravidla přístupu",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Einstellungen teilen",
"adminOptions": "Optionen für Verwaltungsbenutzer",
"shareDuration": "Abgelaufen",
"searchOptions": "Suchoptionen"
"searchOptions": "Suchoptionen",
"downloads": "Herunterladen"
},
"sidebar": {
"help": "Hilfe",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Favicon-URL oder -Pfad, auf den der Client zugreifen kann - es wird entweder ein absoluter oder ein Indexpfad akzeptiert: z. B. 'https://domain.com/favicon.png' oder '/path/to/favicon.png'.",
"hideNavButtons": "Navigationsschaltflächen ausblenden",
"hideNavButtonsDescription": "Blenden Sie die Navigationsschaltflächen auf der Navigationsleiste in der Freigabe aus, um ein minimalistisches Aussehen zu erzielen.",
"viewModeDescription": "Standardlayout für die gemeinsame Seite."
"viewModeDescription": "Standardlayout für die gemeinsame Seite.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Sie haben eine Aktie zur Ansicht oder zum Download erhalten.",
"disableShareCard": "Freigabekarte deaktivieren",
"disableShareCardDescription": "Deaktivieren Sie die Freigabekarte auf der freigegebenen Seite in der Seitenleiste oder oben auf der Seite auf dem Handy.",
"disableSidebar": "Seitenleiste deaktivieren",
"disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite."
},
"api": {
"title": "API-Schlüssel:",
@@ -391,7 +398,6 @@
"toggleClick": "Einzelklick umschalten",
"toggleDark": "Dunkelmodus umschalten",
"toggleSticky": "Sticky-Modus umschalten",
"of": "von",
"used": "belegt",
"noSources": "Für diesen Benutzer sind keine Quellen verfügbar. Entweder eine Quelle für diesen Benutzer hinzufügen oder eine Quelle mit defaultEnabled: true konfigurieren"
},
@@ -418,7 +424,8 @@
"folders": "Ordner",
"exit": "Ausfahrt",
"permanent": "Dauerhaft",
"hash": "Hash"
"hash": "Hash",
"of": "von"
},
"access": {
"rules": "Zugangsregeln",
+11 -4
View File
@@ -300,7 +300,8 @@
"shareSettings": "Ρυθμίσεις κοινής χρήσης",
"adminOptions": "Επιλογές χρήστη διαχειριστή",
"shareDuration": "Λήγει",
"searchOptions": "Επιλογές αναζήτησης"
"searchOptions": "Επιλογές αναζήτησης",
"downloads": "Λήψεις"
},
"sidebar": {
"help": "Βοήθεια",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Διεύθυνση URL ή διαδρομή Favicon προσβάσιμη από τον πελάτη -- γίνονται δεκτές είτε η απόλυτη διαδρομή είτε η διαδρομή ευρετηρίου: π.χ. 'https://domain.com/favicon.png' ή '/path/to/favicon.png'.",
"hideNavButtons": "Απόκρυψη κουμπιών πλοήγησης",
"hideNavButtonsDescription": "Κρύψτε τα κουμπιά πλοήγησης στη γραμμή πλοήγησης στο μερίδιο για να δημιουργήσετε μια μινιμαλιστική εμφάνιση.",
"viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα."
"viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Σας έχει αποσταλεί ένα share για να το δείτε ή να το κατεβάσετε.",
"disableShareCard": "Απενεργοποίηση κάρτας κοινής χρήσης",
"disableShareCardDescription": "Απενεργοποιήστε την κάρτα κοινής χρήσης στην κοινόχρηστη σελίδα στην πλαϊνή μπάρα ή στην κορυφή της σελίδας στο κινητό.",
"disableSidebar": "Απενεργοποίηση πλευρικής μπάρας",
"disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα."
},
"api": {
"title": "Κλειδιά API:",
@@ -391,7 +398,6 @@
"toggleClick": "Εναλλαγή με ένα κλικ",
"toggleDark": "Εναλλαγή σκοτεινής λειτουργίας",
"toggleSticky": "Εναλλαγή της λειτουργίας Sticky",
"of": "του",
"used": "χρησιμοποιημένο",
"noSources": "Δεν υπάρχουν διαθέσιμες πηγές για αυτόν τον χρήστη. Είτε προσθέστε μια πηγή για αυτόν τον χρήστη είτε ρυθμίστε μια πηγή με defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "Φάκελοι",
"exit": "Έξοδος",
"permanent": "Μόνιμο",
"hash": "Κατακερματισμός"
"hash": "Κατακερματισμός",
"of": "του"
},
"access": {
"rules": "Κανόνες πρόσβασης",
+10 -3
View File
@@ -158,7 +158,13 @@
"shareFaviconDescription": "Favicon URL or path accessible by the client -- either abosolute or index path are accepted: eg 'https://domain.com/favicon.png' or '/path/to/favicon.png'",
"hideNavButtons": "Hide navigation buttons",
"hideNavButtonsDescription": "Hide the navigation buttons on the navbar in the share to create a minimalistic look.",
"viewModeDescription": "Default layout for the shared page."
"viewModeDescription": "Default layout for the shared page.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "A share has been sent to you to view or download.",
"disableShareCard": "Disable share card",
"disableShareCardDescription": "Disable the share card on the shared page in the sidebar or at the top of the page on mobile.",
"disableSidebar": "Disable sidebar",
"disableSidebarDescription": "Disable the sidebar on the shared page."
},
"prompts": {
"downloadsLimit": "Number of downloads limit",
@@ -361,6 +367,7 @@
"scope": "Scope",
"settingsUpdated": "Settings updated!",
"shareDuration": "Expires",
"downloads": "Downloads",
"shareManagement": "Share Management",
"shareDeleted": "Share deleted!",
"singleClick": "Use single clicks to open files and directories",
@@ -442,7 +449,6 @@
"toggleClick": "Toggle Single Click",
"toggleDark": "Toggle Dark Mode",
"toggleSticky": "Toggle Sticky Mode",
"of": "of",
"used": "used",
"noSources": "No sources available for this user. Either add a source for this user or configure a source with defaultEnabled: true"
},
@@ -469,7 +475,8 @@
"folders": "Folders",
"exit": "Exit",
"permanent": "Permanent",
"hash": "Hash"
"hash": "Hash",
"of": "of"
},
"access": {
"all": "Deny All",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Compartidos",
"adminOptions": "Opciones de administrador",
"shareDuration": "Expira en",
"searchOptions": "Opciones de búsqueda"
"searchOptions": "Opciones de búsqueda",
"downloads": "Descargas"
},
"sidebar": {
"help": "Ayuda",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL del favicon o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'.",
"hideNavButtons": "Ocultar botones de navegación",
"hideNavButtonsDescription": "Oculte los botones de navegación de la barra de navegación en la acción para crear un aspecto minimalista.",
"viewModeDescription": "Diseño por defecto de la página compartida."
"viewModeDescription": "Diseño por defecto de la página compartida.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Se le ha enviado una acción para que la vea o descargue.",
"disableShareCard": "Desactivar tarjeta compartida",
"disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en la barra lateral o en la parte superior de la página en móvil.",
"disableSidebar": "Desactivar la barra lateral",
"disableSidebarDescription": "Desactivar la barra lateral en la página compartida."
},
"api": {
"title": "Crear API:",
@@ -391,7 +398,6 @@
"toggleClick": "Abrir con un solo clic",
"toggleDark": "Activar modo oscuro",
"toggleSticky": "Barra lateral visible",
"of": "de",
"used": "usado",
"noSources": "No hay fuentes disponibles. Añada una o active defaultEnabled: true."
},
@@ -418,7 +424,8 @@
"folders": "Carpetas",
"exit": "Salida",
"permanent": "Permanente",
"hash": "Hash"
"hash": "Hash",
"of": "de"
},
"access": {
"rules": "Normas de acceso",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Paramètres des partages",
"adminOptions": "Options de l'utilisateur administrateur",
"shareDuration": "Expiration",
"searchOptions": "Options de recherche"
"searchOptions": "Options de recherche",
"downloads": "Téléchargements"
},
"sidebar": {
"help": "Aide",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL du favicon ou chemin d'accès accessible par le client -- les chemins d'accès absolus ou indexés sont acceptés : par exemple \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png\".",
"hideNavButtons": "Masquer les boutons de navigation",
"hideNavButtonsDescription": "Masquez les boutons de navigation de la barre de navigation dans le partage pour créer une apparence minimaliste.",
"viewModeDescription": "Mise en page par défaut de la page partagée."
"viewModeDescription": "Mise en page par défaut de la page partagée.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Une action vous a été envoyée pour que vous la consultiez ou la téléchargiez.",
"disableShareCard": "Désactiver la carte de partage",
"disableShareCardDescription": "Désactiver la carte de partage sur la page partagée dans la barre latérale ou en haut de la page sur mobile.",
"disableSidebar": "Désactiver la barre latérale",
"disableSidebarDescription": "Désactiver la barre latérale sur la page partagée."
},
"api": {
"title": "Clés API :",
@@ -391,7 +398,6 @@
"toggleClick": "Activation du mode simple clic",
"toggleDark": "Mode sombre/clair",
"toggleSticky": "Activer/Désactiver le mode persistent",
"of": "de",
"used": "utilisé",
"noSources": "Aucune source n'est disponible pour cet utilisateur. Ajoutez une source pour cet utilisateur ou configurez une source avec defaultEnabled : true"
},
@@ -418,7 +424,8 @@
"folders": "Dossiers",
"exit": "Sortie",
"permanent": "Permanent",
"hash": "Hachures"
"hash": "Hachures",
"of": "de"
},
"access": {
"rules": "Règles d'accès",
+11 -4
View File
@@ -297,7 +297,8 @@
"api": "מפתחות API",
"adminOptions": "אפשרויות משתמש מנהל",
"shareDuration": "תוקף",
"searchOptions": "אפשרויות חיפוש"
"searchOptions": "אפשרויות חיפוש",
"downloads": "הורדות"
},
"sidebar": {
"help": "עזרה",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "כתובת URL או נתיב של סמל favicon הנגיש ללקוח - ניתן להשתמש בכתובת מוחלטת או בנתיב אינדקס: לדוגמה, 'https://domain.com/favicon.png' או '/path/to/favicon.png'",
"hideNavButtons": "הסתר כפתורי ניווט",
"hideNavButtonsDescription": "הסתר את כפתורי הניווט בסרגל הניווט בשיתוף כדי ליצור מראה מינימליסטי.",
"viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף."
"viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "נשלחה אליך קובץ לשיתוף או להורדה.",
"disableShareCard": "השבת כרטיס שיתוף",
"disableShareCardDescription": "השבת את כרטיס השיתוף בדף המשותף בסרגל הצד או בחלק העליון של הדף במכשירים ניידים.",
"disableSidebar": "השבת סרגל צד",
"disableSidebarDescription": "השבת את סרגל הצד בדף המשותף."
},
"api": {
"title": "מפתחות API",
@@ -391,7 +398,6 @@
"toggleClick": "החלף לחיצה אחת",
"toggleDark": "הפעל מצב כהה",
"toggleSticky": "הפעל/כבה מצב דביק",
"of": "של",
"used": "משומש",
"noSources": "אין מקורות זמינים עבור משתמש זה. הוסף מקור עבור משתמש זה או הגדר מקור עם defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "תיקיות",
"exit": "יציאה",
"permanent": "קבוע",
"hash": "חשיש"
"hash": "חשיש",
"of": "של"
},
"access": {
"rules": "כללי גישה",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Beállítások megosztása",
"adminOptions": "Admin felhasználói beállítások",
"shareDuration": "Lejárat:",
"searchOptions": "Keresési lehetőségek"
"searchOptions": "Keresési lehetőségek",
"downloads": "Letöltések"
},
"sidebar": {
"help": "Súgó",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "A favicon URL címe vagy az ügyfél által elérhető elérési útvonal -- abszolút vagy index útvonal is elfogadható: pl. 'https://domain.com/favicon.png' vagy '/path/to/favicon.png'.",
"hideNavButtons": "Navigációs gombok elrejtése",
"hideNavButtonsDescription": "Rejtse el a navigációs gombokat a megosztás navigációs sávján a minimalista megjelenés érdekében.",
"viewModeDescription": "A megosztott oldal alapértelmezett elrendezése."
"viewModeDescription": "A megosztott oldal alapértelmezett elrendezése.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Megosztást küldtünk Önnek megtekintésre vagy letöltésre.",
"disableShareCard": "Megosztási kártya letiltása",
"disableShareCardDescription": "Kapcsolja ki a megosztási kártyát a megosztott oldalon az oldalsávban vagy mobilon az oldal tetején.",
"disableSidebar": "Oldalsáv kikapcsolása",
"disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon."
},
"api": {
"title": "API-kulcsok:",
@@ -391,7 +398,6 @@
"toggleClick": "Egyetlen kattintás kapcsolása",
"toggleDark": "Sötét üzemmód átkapcsolása",
"toggleSticky": "Ragadós mód átkapcsolása",
"of": "a",
"used": "használt",
"noSources": "Ehhez a felhasználóhoz nem állnak rendelkezésre források. Vagy adjon hozzá egy forrást ehhez a felhasználóhoz, vagy konfiguráljon egy forrást a defaultEnabled: true beállítással."
},
@@ -418,7 +424,8 @@
"folders": "Mappák",
"exit": "Kilépés",
"permanent": "Állandó",
"hash": "Hash"
"hash": "Hash",
"of": "a"
},
"access": {
"rules": "Hozzáférési szabályok",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Condividi le impostazioni",
"adminOptions": "Opzioni dell'utente amministratore",
"shareDuration": "Scadenza",
"searchOptions": "Opzioni di ricerca"
"searchOptions": "Opzioni di ricerca",
"downloads": "Scaricamento"
},
"sidebar": {
"help": "Aiuto",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL o percorso della favicon accessibile dal client -- sono accettati sia il percorso assoluto che quello indice: ad esempio 'https://domain.com/favicon.png' o '/path/to/favicon.png'.",
"hideNavButtons": "Nascondere i pulsanti di navigazione",
"hideNavButtonsDescription": "Nascondere i pulsanti di navigazione sulla navbar nella condivisione per creare un look minimalista.",
"viewModeDescription": "Layout predefinito per la pagina condivisa."
"viewModeDescription": "Layout predefinito per la pagina condivisa.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "È stata inviata una condivisione da visualizzare o scaricare.",
"disableShareCard": "Disattivare la scheda di condivisione",
"disableShareCardDescription": "Disattivare la scheda di condivisione sulla pagina condivisa nella barra laterale o nella parte superiore della pagina su mobile.",
"disableSidebar": "Disattivare la barra laterale",
"disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa."
},
"api": {
"title": "Chiavi API:",
@@ -391,7 +398,6 @@
"toggleClick": "Alterna il clic singolo",
"toggleDark": "Alterna la modalità scura",
"toggleSticky": "Alterna la modalità appiccicosa",
"of": "di",
"used": "usato",
"noSources": "Non ci sono fonti disponibili per questo utente. Aggiungere una fonte per questo utente o configurare una fonte con defaultEnabled: true."
},
@@ -418,7 +424,8 @@
"folders": "Cartelle",
"exit": "Uscita",
"permanent": "Permanente",
"hash": "Hash"
"hash": "Hash",
"of": "di"
},
"access": {
"rules": "Regole di accesso",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "共有設定",
"adminOptions": "管理者ユーザーオプション",
"shareDuration": "期限切れ",
"searchOptions": "検索オプション"
"searchOptions": "検索オプション",
"downloads": "ダウンロード"
},
"sidebar": {
"help": "ヘルプ",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "クライアントからアクセス可能なファビコンのURLまたはパス -- 絶対パスでもインデックスパスでも構いません: 'https://domain.com/favicon.png' または '/path/to/favicon.png' など。",
"hideNavButtons": "ナビゲーションボタンを隠す",
"hideNavButtonsDescription": "シェア内のナビバーにあるナビゲーションボタンを非表示にして、ミニマルな外観を作る。",
"viewModeDescription": "共有ページのデフォルトレイアウト。"
"viewModeDescription": "共有ページのデフォルトレイアウト。",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "シェアが送信されましたので、閲覧またはダウンロードしてください。",
"disableShareCard": "シェアカードを無効にする",
"disableShareCardDescription": "サイドバーの共有ページ、またはモバイルのページ上部にある共有カードを無効にする。",
"disableSidebar": "サイドバーを無効にする",
"disableSidebarDescription": "共有ページのサイドバーを無効にする。"
},
"api": {
"title": "APIキー:",
@@ -391,7 +398,6 @@
"toggleClick": "トグル・シングルクリック",
"toggleDark": "ダークモードの切り替え",
"toggleSticky": "スティッキーモードの切り替え",
"of": "の",
"used": "中古",
"noSources": "このユーザが使用できるソースはありません。このユーザのソースを追加するか、defaultEnabled: true でソースを構成します。"
},
@@ -418,7 +424,8 @@
"folders": "フォルダ",
"exit": "出口",
"permanent": "正社員",
"hash": "ハッシュ"
"hash": "ハッシュ",
"of": "の"
},
"access": {
"rules": "アクセス規則",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "공유 설정",
"adminOptions": "관리자 사용자 옵션",
"shareDuration": "만료",
"searchOptions": "검색 옵션"
"searchOptions": "검색 옵션",
"downloads": "다운로드"
},
"sidebar": {
"help": "도움말",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "클라이언트가 액세스할 수 있는 파비콘 URL 또는 경로 - 절대 경로 또는 인덱스 경로(예: 'https://domain.com/favicon.png' 또는 '/path/to/favicon.png')가 허용됩니다.",
"hideNavButtons": "탐색 버튼 숨기기",
"hideNavButtonsDescription": "공유의 탐색 모음에서 탐색 버튼을 숨겨 미니멀한 디자인을 만듭니다.",
"viewModeDescription": "공유 페이지의 기본 레이아웃입니다."
"viewModeDescription": "공유 페이지의 기본 레이아웃입니다.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "보거나 다운로드할 수 있는 공유물이 전송되었습니다.",
"disableShareCard": "공유 카드 비활성화",
"disableShareCardDescription": "사이드바의 공유 페이지 또는 모바일의 페이지 상단에서 공유 카드를 비활성화합니다.",
"disableSidebar": "사이드바 비활성화",
"disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다."
},
"api": {
"title": "API 키:",
@@ -391,7 +398,6 @@
"toggleClick": "클릭 한 번으로 토글",
"toggleDark": "다크 모드 전환",
"toggleSticky": "고정 모드 토글",
"of": "의",
"used": "사용됨",
"noSources": "이 사용자에 대해 사용할 수 있는 소스가 없습니다. 이 사용자에 대한 소스를 추가하거나 defaultEnabled: true로 소스를 구성하세요."
},
@@ -418,7 +424,8 @@
"folders": "폴더",
"exit": "종료",
"permanent": "영구",
"hash": "해시"
"hash": "해시",
"of": "의"
},
"access": {
"rules": "액세스 규칙",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareDeleted": "Deel verwijderd!",
"adminOptions": "Opties voor beheerders",
"shareDuration": "Verloopt op",
"searchOptions": "Zoekopties"
"searchOptions": "Zoekopties",
"downloads": "Downloads"
},
"sidebar": {
"help": "Help",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Favicon URL of pad toegankelijk door de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/favicon.png' of '/path/to/favicon.png'.",
"hideNavButtons": "Navigatieknoppen verbergen",
"hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de share om een minimalistische look te creëren.",
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina."
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Er is een share naar je verzonden om te bekijken of te downloaden.",
"disableShareCard": "Aandelenkaart uitschakelen",
"disableShareCardDescription": "Schakel de deelkaart uit op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.",
"disableSidebar": "Zijbalk uitschakelen",
"disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit."
},
"api": {
"title": "API-sleutels",
@@ -391,7 +398,6 @@
"toggleClick": "Schakelen Enkele klik",
"toggleDark": "Donkere modus inschakelen",
"toggleSticky": "Kleefmodus schakelen",
"of": "van",
"used": "tweedehands",
"noSources": "Geen bronnen beschikbaar voor deze gebruiker. Voeg een bron toe voor deze gebruiker of configureer een bron met defaultEnabled: true"
},
@@ -484,7 +490,8 @@
"folders": "Mappen",
"exit": "Ga naar",
"permanent": "Permanent",
"hash": "Hash"
"hash": "Hash",
"of": "van"
},
"access": {
"rules": "Toegangsregels",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Ustawienia udostępniania",
"adminOptions": "Opcje użytkownika administratora",
"shareDuration": "Wygasa",
"searchOptions": "Opcje wyszukiwania"
"searchOptions": "Opcje wyszukiwania",
"downloads": "Pliki do pobrania"
},
"sidebar": {
"help": "Pomoc",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Adres URL Favicon lub ścieżka dostępna dla klienta - akceptowana jest ścieżka bezwzględna lub indeksowa: np. \"https://domain.com/favicon.png\" lub \"/path/to/favicon.png\".",
"hideNavButtons": "Ukryj przyciski nawigacyjne",
"hideNavButtonsDescription": "Ukryj przyciski nawigacyjne na pasku nawigacyjnym w folderze udostępnionym, aby uzyskać minimalistyczny wygląd.",
"viewModeDescription": "Domyślny układ strony udostępnionej."
"viewModeDescription": "Domyślny układ strony udostępnionej.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Udział został wysłany do wyświetlenia lub pobrania.",
"disableShareCard": "Wyłączanie karty udostępniania",
"disableShareCardDescription": "Wyłącz kartę udostępniania na udostępnianej stronie na pasku bocznym lub u góry strony na urządzeniach mobilnych.",
"disableSidebar": "Wyłącz pasek boczny",
"disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie."
},
"api": {
"title": "Klucze API:",
@@ -391,7 +398,6 @@
"toggleClick": "Przełączanie jednym kliknięciem",
"toggleDark": "Przełączanie trybu ciemnego",
"toggleSticky": "Przełączanie trybu samoprzylepnego",
"of": "z",
"used": "używany",
"noSources": "Brak dostępnych źródeł dla tego użytkownika. Dodaj źródło dla tego użytkownika lub skonfiguruj źródło z defaultEnabled: true."
},
@@ -418,7 +424,8 @@
"folders": "Foldery",
"exit": "Wyjście",
"permanent": "Na stałe",
"hash": "Hash"
"hash": "Hash",
"of": "z"
},
"access": {
"rules": "Zasady dostępu",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Compartilhar configurações",
"adminOptions": "Opções do usuário administrador",
"shareDuration": "Expirações",
"searchOptions": "Opções de pesquisa"
"searchOptions": "Opções de pesquisa",
"downloads": "Downloads"
},
"sidebar": {
"help": "Ajuda",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL ou caminho do Favicon acessível pelo cliente - são aceitos caminhos abosolutos ou de índice: por exemplo, \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png\".",
"hideNavButtons": "Ocultar botões de navegação",
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação do compartilhamento para criar uma aparência minimalista.",
"viewModeDescription": "Layout padrão para a página compartilhada."
"viewModeDescription": "Layout padrão para a página compartilhada.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Um compartilhamento foi enviado a você para visualização ou download.",
"disableShareCard": "Desativar cartão compartilhado",
"disableShareCardDescription": "Desative o cartão de compartilhamento na página compartilhada na barra lateral ou na parte superior da página no celular.",
"disableSidebar": "Desativar a barra lateral",
"disableSidebarDescription": "Desative a barra lateral na página compartilhada."
},
"api": {
"title": "Chaves de API:",
@@ -391,7 +398,6 @@
"toggleClick": "Alternar clique único",
"toggleDark": "Alternar o modo escuro",
"toggleSticky": "Alternar o modo de aderência",
"of": "de",
"used": "usado",
"noSources": "Não há fontes disponíveis para esse usuário. Adicione uma fonte para esse usuário ou configure uma fonte com defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "Pastas",
"exit": "Sair",
"permanent": "Permanente",
"hash": "Hash"
"hash": "Hash",
"of": "de"
},
"access": {
"rules": "Regras de acesso",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareDeleted": "Partilhar eliminado!",
"adminOptions": "Opções do utilizador administrador",
"shareDuration": "Expirações",
"searchOptions": "Opções de pesquisa"
"searchOptions": "Opções de pesquisa",
"downloads": "Transferências"
},
"sidebar": {
"help": "Ajuda",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL ou caminho do Favicon acessível pelo cliente - são aceites caminhos abosolutos ou de índice: por exemplo, \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png",
"hideNavButtons": "Ocultar botões de navegação",
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação na partilha para criar um aspeto minimalista.",
"viewModeDescription": "Esquema predefinido para a página partilhada."
"viewModeDescription": "Esquema predefinido para a página partilhada.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Foi-lhe enviada uma ação para visualizar ou descarregar.",
"disableShareCard": "Desativar o cartão de partilha",
"disableShareCardDescription": "Desativar o cartão de partilha na página partilhada na barra lateral ou na parte superior da página no telemóvel.",
"disableSidebar": "Desativar a barra lateral",
"disableSidebarDescription": "Desativar a barra lateral na página partilhada."
},
"api": {
"title": "Chaves da API",
@@ -391,7 +398,6 @@
"toggleClick": "Alternar clique único",
"toggleDark": "Alternar o modo escuro",
"toggleSticky": "Alternar o modo de aderência",
"of": "de",
"used": "utilizado",
"noSources": "Não existem fontes disponíveis para este utilizador. Adicione uma fonte para este utilizador ou configure uma fonte com defaultEnabled: true"
},
@@ -484,7 +490,8 @@
"folders": "Pastas",
"exit": "Sair",
"permanent": "Permanente",
"hash": "Haxixe"
"hash": "Haxixe",
"of": "de"
},
"access": {
"rules": "Regras de acesso",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Setări de partajare",
"adminOptions": "Opțiuni utilizator administrator",
"shareDuration": "Expiră",
"searchOptions": "Opțiuni de căutare"
"searchOptions": "Opțiuni de căutare",
"downloads": "Descărcări"
},
"sidebar": {
"help": "Ajutor",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Favicon URL sau cale accesibilă de către client - sunt acceptate fie calea abosolută, fie calea index: de exemplu \"https://domain.com/favicon.png\" sau \"/path/to/favicon.png",
"hideNavButtons": "Ascundeți butoanele de navigare",
"hideNavButtonsDescription": "Ascundeți butoanele de navigare de pe bara de navigare din share pentru a crea un aspect minimalist.",
"viewModeDescription": "Layout implicit pentru pagina partajată."
"viewModeDescription": "Layout implicit pentru pagina partajată.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "V-a fost trimisă o acțiune pentru a o vizualiza sau descărca.",
"disableShareCard": "Dezactivați cardul de partajare",
"disableShareCardDescription": "Dezactivați cardul de partajare pe pagina partajată în bara laterală sau în partea de sus a paginii pe mobil.",
"disableSidebar": "Dezactivați bara laterală",
"disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată."
},
"api": {
"title": "Chei API:",
@@ -391,7 +398,6 @@
"toggleClick": "Comutare clic unic",
"toggleDark": "Comutarea modului întunecat",
"toggleSticky": "Schimbarea modului lipicios",
"of": "de",
"used": "utilizate",
"noSources": "Nu există surse disponibile pentru acest utilizator. Adăugați o sursă pentru acest utilizator sau configurați o sursă cu defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "Dosare",
"exit": "Ieșire",
"permanent": "Permanent",
"hash": "Hash"
"hash": "Hash",
"of": "de"
},
"access": {
"rules": "Reguli de acces",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Поделиться настройками",
"adminOptions": "Параметры пользователя администратора",
"shareDuration": "Срок действия:",
"searchOptions": "Параметры поиска"
"searchOptions": "Параметры поиска",
"downloads": "Скачать"
},
"sidebar": {
"help": "Помощь",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL-адрес фавикона или путь, доступный клиенту - принимается либо абсолютный, либо индексный путь: например, 'https://domain.com/favicon.png' или '/path/to/favicon.png'",
"hideNavButtons": "Скрыть кнопки навигации",
"hideNavButtonsDescription": "Спрячьте кнопки навигации на навигационной панели в ресурсе, чтобы создать минималистичный вид.",
"viewModeDescription": "Макет по умолчанию для общей страницы."
"viewModeDescription": "Макет по умолчанию для общей страницы.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Вам отправлена акция для просмотра или загрузки.",
"disableShareCard": "Отключение карты доступа",
"disableShareCardDescription": "Отключите карточку общего доступа на странице общего доступа в боковой панели или в верхней части страницы на мобильных устройствах.",
"disableSidebar": "Отключить боковую панель",
"disableSidebarDescription": "Отключите боковую панель на общей странице."
},
"api": {
"title": "Ключи API:",
@@ -391,7 +398,6 @@
"toggleClick": "Переключение одним щелчком",
"toggleDark": "Переключение темного режима",
"toggleSticky": "Переключение режима липкости",
"of": "из",
"used": "используется",
"noSources": "Для этого пользователя нет доступных источников. Либо добавьте источник для этого пользователя, либо настройте источник с параметром defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "Папки",
"exit": "Выход",
"permanent": "Постоянно",
"hash": "Хаш"
"hash": "Хаш",
"of": "из"
},
"access": {
"rules": "Правила доступа",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Zdieľať nastavenia",
"adminOptions": "Možnosti používateľa správcu",
"shareDuration": "Platnosť končí na",
"searchOptions": "Možnosti vyhľadávania"
"searchOptions": "Možnosti vyhľadávania",
"downloads": "Na stiahnutie"
},
"sidebar": {
"help": "Pomoc",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Adresa URL alebo cesta k ikone Favicon, ktorá je prístupná klientovi -- akceptuje sa buď absolútna, alebo indexová cesta: napr. \"https://domain.com/favicon.png\" alebo \"/path/to/favicon.png\".",
"hideNavButtons": "Skrytie navigačných tlačidiel",
"hideNavButtonsDescription": "Skryte navigačné tlačidlá na paneli navigácie v zdieľaní, aby ste vytvorili minimalistický vzhľad.",
"viewModeDescription": "Predvolené rozloženie zdieľanej stránky."
"viewModeDescription": "Predvolené rozloženie zdieľanej stránky.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Bola vám odoslaná akcia na zobrazenie alebo stiahnutie.",
"disableShareCard": "Zakázanie zdieľanej karty",
"disableShareCardDescription": "Zakážte kartu zdieľania na zdieľanej stránke v bočnom paneli alebo v hornej časti stránky na mobilnom zariadení.",
"disableSidebar": "Zakázanie bočného panela",
"disableSidebarDescription": "Zakážte bočný panel na zdieľanej stránke."
},
"api": {
"title": "Kľúče API:",
@@ -391,7 +398,6 @@
"toggleClick": "Prepínanie jedným kliknutím",
"toggleDark": "Prepínanie tmavého režimu",
"toggleSticky": "Prepnutie režimu Sticky",
"of": "z",
"used": "použité",
"noSources": "Pre tohto používateľa nie sú k dispozícii žiadne zdroje. Buď pridajte zdroj pre tohto používateľa, alebo nakonfigurujte zdroj s defaultEnabled: true"
},
@@ -418,7 +424,8 @@
"folders": "Priečinky",
"exit": "Exit",
"permanent": "Trvalé",
"hash": "Hash"
"hash": "Hash",
"of": "z"
},
"access": {
"rules": "Pravidlá prístupu",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareDeleted": "Dela borttaget!",
"adminOptions": "Alternativ för administratörsanvändare",
"shareDuration": "Upphör att gälla",
"searchOptions": "Sökalternativ"
"searchOptions": "Sökalternativ",
"downloads": "Nedladdningar"
},
"sidebar": {
"help": "Hjälp",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL eller sökväg till Favicon som är tillgänglig för klienten - antingen abosolute eller indexsökväg accepteras: t.ex. \"https://domain.com/favicon.png\" eller \"/path/to/favicon.png",
"hideNavButtons": "Dölj navigeringsknappar",
"hideNavButtonsDescription": "Dölj navigeringsknapparna i navigeringsfältet i aktien för att skapa ett minimalistiskt utseende.",
"viewModeDescription": "Standardlayout för den delade sidan."
"viewModeDescription": "Standardlayout för den delade sidan.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "En aktie har skickats till dig för visning eller nedladdning.",
"disableShareCard": "Inaktivera aktiekort",
"disableShareCardDescription": "Inaktivera delningskortet på den delade sidan i sidofältet eller högst upp på sidan på mobilen.",
"disableSidebar": "Inaktivera sidofält",
"disableSidebarDescription": "Inaktivera sidofältet på den delade sidan."
},
"api": {
"title": "API-nycklar",
@@ -391,7 +398,6 @@
"toggleClick": "Toggle Enkelklick",
"toggleDark": "Växla till mörkt läge",
"toggleSticky": "Växla klisterläge",
"of": "av",
"used": "används",
"noSources": "Inga källor tillgängliga för den här användaren. Lägg antingen till en källa för den här användaren eller konfigurera en källa med defaultEnabled: true"
},
@@ -484,7 +490,8 @@
"folders": "Mappar",
"exit": "Avsluta",
"permanent": "Permanent",
"hash": "Hash"
"hash": "Hash",
"of": "av"
},
"access": {
"rules": "Regler för åtkomst",
+11 -4
View File
@@ -297,7 +297,8 @@
"shareSettings": "Paylaşım Ayarları",
"adminOptions": "Yönetici kullanıcı seçenekleri",
"shareDuration": "Sona erdi",
"searchOptions": "Arama seçenekleri"
"searchOptions": "Arama seçenekleri",
"downloads": "İndirmeler"
},
"sidebar": {
"help": "Yardım",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "Favicon URL'si veya istemci tarafından erişilebilen yol -- mutlak veya dizin yolu kabul edilir: örneğin 'https://domain.com/favicon.png' veya '/path/to/favicon.png'",
"hideNavButtons": "Gezinme düğmelerini gizle",
"hideNavButtonsDescription": "Minimalist bir görünüm oluşturmak için paylaşımdaki gezinme çubuğundaki gezinme düğmelerini gizleyin.",
"viewModeDescription": "Paylaşılan sayfa için varsayılan düzen."
"viewModeDescription": "Paylaşılan sayfa için varsayılan düzen.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Görüntülemeniz veya indirmeniz için size bir paylaşım gönderildi.",
"disableShareCard": "Paylaşım kartını devre dışı bırak",
"disableShareCardDescription": "Kenar çubuğunda paylaşılan sayfadaki veya mobil cihazlarda sayfanın üst kısmındaki paylaşım kartını devre dışı bırakın.",
"disableSidebar": "Kenar çubuğunu devre dışı bırak",
"disableSidebarDescription": "Paylaşılan sayfadaki kenar çubuğunu devre dışı bırakın."
},
"api": {
"title": "API Anahtarları:",
@@ -391,7 +398,6 @@
"toggleClick": "Tek Tıklamayı Değiştir",
"toggleDark": "Karanlık Modu Aç / Kapat",
"toggleSticky": "Yapışkan Modunu Değiştir",
"of": ".",
"used": "kullanılmış",
"noSources": "Bu kullanıcı için kullanılabilir kaynak yok. Ya bu kullanıcı için bir kaynak ekleyin ya da defaultEnabled: true ile bir kaynak yapılandırın"
},
@@ -418,7 +424,8 @@
"folders": "Klasörler",
"exit": "Çıkış",
"permanent": "Daimi",
"hash": "Hash"
"hash": "Hash",
"of": "."
},
"access": {
"rules": "Erişim kuralları",
+11 -4
View File
@@ -297,7 +297,8 @@
"api": "Ключі API",
"adminOptions": "Параметри користувача адміністрування",
"shareDuration": "Закінчується",
"searchOptions": "Параметри пошуку"
"searchOptions": "Параметри пошуку",
"downloads": "Завантаження"
},
"sidebar": {
"help": "Допомога",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "URL-адреса іконки або шлях, доступний клієнту - допускається як абсолютний, так і індексний шлях: наприклад, 'https://domain.com/favicon.png' або '/путь/до/favicon.png'.",
"hideNavButtons": "Приховати кнопки навігації",
"hideNavButtonsDescription": "Приховайте кнопки навігації на панелі навігації у спільному доступі, щоб створити мінімалістичний вигляд.",
"viewModeDescription": "Макет за замовчуванням для спільної сторінки."
"viewModeDescription": "Макет за замовчуванням для спільної сторінки.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Вам надіслано файл для перегляду або завантаження.",
"disableShareCard": "Вимкнути картку спільного доступу",
"disableShareCardDescription": "Вимкніть картку спільного доступу на сторінці, до якої надається спільний доступ, на бічній панелі або вгорі сторінки на мобільних пристроях.",
"disableSidebar": "Вимкнути бічну панель",
"disableSidebarDescription": "Вимкнути бічну панель на спільній сторінці."
},
"api": {
"title": "Ключі API",
@@ -391,7 +398,6 @@
"toggleClick": "Перемикання одним клацанням миші",
"toggleDark": "Увімкнути темний режим",
"toggleSticky": "Увімкнути липкий режим",
"of": "з",
"used": "використаний",
"noSources": "Для цього користувача немає доступних джерел. Або додайте джерело для цього користувача, або налаштуйте джерело з defaultEnabled: true"
},
@@ -484,7 +490,8 @@
"folders": "Папки",
"exit": "Виходьте.",
"permanent": "Постійний",
"hash": "Хеш"
"hash": "Хеш",
"of": "з"
},
"access": {
"rules": "Правила доступу",
+11 -4
View File
@@ -297,7 +297,8 @@
"api": "API 密钥",
"adminOptions": "管理员用户选项",
"shareDuration": "到期",
"searchOptions": "搜索选项"
"searchOptions": "搜索选项",
"downloads": "下载"
},
"sidebar": {
"help": "帮助",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "客户端可访问的 Favicon URL 或路径--接受绝对路径或索引路径:例如 \"https://domain.com/favicon.png \"或\"/path/to/favicon.png\"。",
"hideNavButtons": "隐藏导航按钮",
"hideNavButtonsDescription": "隐藏共享导航栏上的导航按钮,打造简约外观。",
"viewModeDescription": "共享页面的默认布局。"
"viewModeDescription": "共享页面的默认布局。",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "您已收到一份共享文件,供您查看或下载。",
"disableShareCard": "禁用共享卡",
"disableShareCardDescription": "禁用共享页面侧边栏或手机页面顶部的共享卡。",
"disableSidebar": "禁用侧边栏",
"disableSidebarDescription": "禁用共享页面上的边栏。"
},
"api": {
"title": "应用程序接口密钥",
@@ -391,7 +398,6 @@
"toggleClick": "单击切换",
"toggleDark": "切换黑暗模式",
"toggleSticky": "切换粘贴模式",
"of": "的",
"used": "中古",
"noSources": "该用户没有可用的信息源。要么为该用户添加一个信息源,要么配置一个默认为 true 的信息源。"
},
@@ -484,7 +490,8 @@
"folders": "文件夹",
"exit": "退出",
"permanent": "永久性",
"hash": "散列"
"hash": "散列",
"of": "的"
},
"access": {
"rules": "访问规则",
+11 -4
View File
@@ -297,7 +297,8 @@
"api": "API 金鑰",
"shareDuration": "到期",
"shareDeleted": "分享已刪除!",
"adminOptions": "管理員使用者選項"
"adminOptions": "管理員使用者選項",
"downloads": "下載"
},
"sidebar": {
"help": "幫助",
@@ -354,7 +355,13 @@
"shareFaviconDescription": "用戶端可存取的 Favicon URL 或路徑 - 接受絕對路徑或索引路徑:例如「https://domain.com/favicon.png」或「/path/to/favicon.png",
"hideNavButtons": "隱藏導覽按鈕",
"hideNavButtonsDescription": "隱藏分享中導覽列上的導覽按鈕,以創造簡約的外觀。",
"viewModeDescription": "共用頁面的預設版面。"
"viewModeDescription": "共用頁面的預設版面。",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "已向您發送一份共用檔案,供您檢視或下載。",
"disableShareCard": "停用分享卡",
"disableShareCardDescription": "停用分享頁面側邊欄或行動版頁面頂端的分享卡。",
"disableSidebar": "停用側邊欄",
"disableSidebarDescription": "停用共用頁面上的側邊欄。"
},
"api": {
"title": "API 金鑰",
@@ -391,7 +398,6 @@
"toggleClick": "切換單擊",
"toggleDark": "切換黑暗模式",
"toggleSticky": "切換黏貼模式",
"of": "的",
"used": "中古",
"noSources": "此使用者沒有可用的來源。請為此使用者新增來源,或設定預設啟用:true 的來源。"
},
@@ -484,7 +490,8 @@
"folders": "資料夾",
"exit": "離開",
"permanent": "長期",
"hash": "散列"
"hash": "散列",
"of": "的"
},
"access": {
"rules": "存取規則",
+64 -16
View File
@@ -1,38 +1,75 @@
import { mutations, state } from '@/store'
let active = false
/** @type {ReturnType<typeof setTimeout> | null} */
let closeTimeout // Store timeout ID
/**
* Show a popup notification
* @param {'success' | 'error' | 'action'} type
* @param {unknown} message
* @param {boolean} [autoclose=true]
*/
export function showPopup(type, message, autoclose = true) {
if (active) {
clearTimeout(closeTimeout) // Clear the existing timeout
if (closeTimeout) clearTimeout(closeTimeout) // Clear the existing timeout
}
const [popup, popupContent] = getElements()
if (popup === undefined || popup == null) {
if (popup == null || popupContent == null) {
return
}
popup.classList.remove('success', 'error') // Clear previous types
popup.classList.add(type)
/** @type {HTMLElement} */
// @ts-ignore - narrow Element to HTMLElement after null check
const popupEl = popup
/** @type {HTMLElement} */
// @ts-ignore - narrow Element to HTMLElement after null check
const popupContentEl = popupContent
popupEl.classList.remove('success', 'error') // Clear previous types
popupEl.classList.add(type)
active = true
let apiMessage
try {
apiMessage = JSON.parse(message)
// Normalize message to a string first to avoid calling string methods on non-strings
const normalizedMessage =
message instanceof Error
? message.toString() // e.g. "Error: {...}"
: typeof message === 'string'
? message
: JSON.stringify(message)
let apiMessage
// check if message starts with "Error: "
if (normalizedMessage.startsWith('Error: ')) {
const errorMessage = normalizedMessage.replace(/^Error:\s*/, '')
apiMessage = JSON.parse(errorMessage)
} else {
apiMessage = JSON.parse(normalizedMessage)
}
if (
apiMessage &&
typeof apiMessage === 'object' &&
Object.prototype.hasOwnProperty.call(apiMessage, 'status') &&
Object.prototype.hasOwnProperty.call(apiMessage, 'message')
) {
popupContent.textContent =
'Error ' + apiMessage.status + ': ' + apiMessage.message
popupContentEl.textContent =
apiMessage.status + ': ' + apiMessage.message
} else {
// Fallback to showing the normalized message if it is not an API error shape
popupContentEl.textContent = normalizedMessage
}
} catch (error) {
popupContent.textContent = message
// Fallback to a safe string representation
const fallback =
message instanceof Error
? message.message || message.toString()
: typeof message === 'string'
? message
: JSON.stringify(message)
popupContentEl.textContent = fallback
}
popup.style.right = '0em'
popupEl.style.right = '0em'
// Don't auto-hide for 'action' type popups
if (type === 'action') {
@@ -55,19 +92,28 @@ export function showPopup(type, message, autoclose = true) {
export function closePopUp() {
active = false
const [popup, popupContent] = getElements()
if (popupContent == undefined) {
if (popup == null || popupContent == null) {
return
}
/** @type {HTMLElement} */
// @ts-ignore - narrow Element to HTMLElement after null check
const popupEl = popup
/** @type {HTMLElement} */
// @ts-ignore - narrow Element to HTMLElement after null check
const popupContentEl = popupContent
if (
popupContent.textContent == 'Multiple Selection Enabled' &&
popupContentEl.textContent == 'Multiple Selection Enabled' &&
state.multiple
) {
mutations.setMultiple(false)
}
popup.style.right = '-50em' // Slide out
popupContent.textContent = 'no content'
popupEl.style.right = '-50em' // Slide out
popupContentEl.textContent = 'no content'
}
/**
* @returns {[Element | null, Element | null]}
*/
function getElements() {
const popup = document.getElementById('popup-notification')
if (!popup) {
@@ -82,10 +128,12 @@ function getElements() {
return [popup, popupContent]
}
/** @param {unknown} message */
export function showSuccess(message) {
showPopup('success', message)
}
/** @param {unknown} message */
export function showError(message) {
showPopup('error', message)
console.error(message)
+8 -9
View File
@@ -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 } from '@/utils/constants.js'
import { noAuth, shareOverrides } from '@/utils/constants.js'
import { getTypeInfo } from '@/utils/mimetype'
import { fromNow } from '@/utils/moment'
import * as i18n from '@/i18n'
@@ -41,8 +41,7 @@ export const getters = {
isLoading: () => Object.keys(state.loading).length > 0,
isSettings: () => getters.currentView() === 'settings',
isShare: () => {
const pathname = getters.routePath()
return pathname.startsWith(`/public/share`)
return shareOverrides.isShare
},
isDarkMode: () => {
if (state.user == null) {
@@ -138,6 +137,9 @@ export const getters = {
return { dirs, files }
},
isSidebarVisible: () => {
if (shareOverrides.disableSidebar) {
return false
}
const cv = getters.currentView()
if (cv == 'onlyOfficeEditor') {
return false
@@ -152,9 +154,6 @@ export const getters = {
]
let visible = (state.showSidebar || getters.isStickySidebar())
if (getters.isShare() && !state.isMobile) {
visible = true
}
if (getters.currentPromptName() && !getters.isStickySidebar()) {
visible = false
}
@@ -172,9 +171,6 @@ export const getters = {
if (currentView == '' && !getters.isLoading()) {
sticky = true
}
if (getters.isShare()) {
sticky = true
}
if (getters.isMobile()) {
sticky = false
}
@@ -348,6 +344,9 @@ export const getters = {
return false
},
officeViewingDisabled: filename => {
if (shareOverrides.isShare) {
return true
}
const ext = ' ' + getFileExtension(filename)
if (state.user.disableOfficePreviewExt) {
const disabledExts = ' ' + state.user.disableOfficePreviewExt.toLowerCase()
+11 -3
View File
@@ -70,7 +70,7 @@ import MarkdownViewer from "./files/MarkdownViewer.vue";
import { state, mutations, getters } from "@/store";
import { url } from "@/utils";
import router from "@/router";
import { baseURL } from "@/utils/constants";
import { baseURL, shareOverrides } from "@/utils/constants";
import PopupPreview from "@/components/files/PopupPreview.vue";
import { extractSourceFromPath } from "@/utils/url";
import ShareInfo from "@/components/files/ShareInfo.vue";
@@ -110,7 +110,7 @@ export default {
return state.share;
},
showShareInfo() {
return this.isShare && state.share.hash && state.isMobile && state.req.path == "/";
return this.isShare && state.share.hash && state.isMobile && state.req.path == "/" && !shareOverrides.disableShareCard;
},
isShare() {
return getters.isShare();
@@ -157,7 +157,15 @@ export default {
if (window.location.hash === this.lastHash) return;
this.lastHash = window.location.hash;
if (window.location.hash) {
const id = url.base64Encode(window.location.hash.slice(1));
const rawHash = window.location.hash.slice(1);
let decodedName = rawHash;
try {
decodedName = decodeURIComponent(rawHash);
} catch (e) {
// If the hash contains malformed escape sequences, fall back to raw
decodedName = rawHash;
}
const id = url.base64Encode(encodeURIComponent(decodedName));
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
+4 -1
View File
@@ -19,12 +19,14 @@
:disabled="isDisabled"
/>
<action
class="overflow-menu-button"
v-else-if="!isListingView && !showQuickSave"
:icon="iconName"
:disabled="noItems"
@click="toggleOverflow"
/>
<action
class="save-button"
v-else-if="showQuickSave"
id="save-button"
icon="save"
@@ -113,7 +115,8 @@ export default {
return state.isSearchActive || getters.currentPromptName() != "";
},
isDisabledMultiAction() {
return this.isDisabled || (getters.isStickySidebar() && getters.multibuttonState() === "menu");
const shareDisabled = shareOverrides.disableSidebar && getters.multibuttonState() === "menu";
return this.isDisabled || (getters.isStickySidebar() && getters.multibuttonState() === "menu") || shareDisabled;
},
showSwitchView() {
return getters.currentView() === "listingView";
+8 -2
View File
@@ -64,9 +64,15 @@ export default {
methods: {
setupEditor(attempt = 1) {
try {
this.filename = decodeURIComponent(this.$route.path.split("/").pop());
this.filename = decodeURIComponent(this.$route.path.split("/").pop() || "");
// Safety Check 1: Use the component's 'filename' data property for comparison
if (state.req.name !== this.filename) {
// no need to do safety check for direct link
if (getters.shareHash() == this.filename) {
this.filename = "";
}
if (this.filename && state.req.name !== this.filename) {
if (attempt < 5) {
console.warn(
`[Attempt ${attempt}/5] State filename ("${state.req.name}") does not match route filename ("${this.filename}"). Retrying in 500ms...`
+10 -3
View File
@@ -591,7 +591,10 @@ export default {
if (state.isSearchActive || getters.currentView() != "listingView" || getters.currentPromptName()) {
return;
}
const { key, ctrlKey, metaKey, which } = event;
const { key, ctrlKey, metaKey, altKey, which } = event;
if (altKey) {
return;
}
// Check if the key is alphanumeric
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
const modifierKeys = ctrlKey || metaKey;
@@ -615,7 +618,7 @@ export default {
if (getters.currentPromptName()) {
return;
}
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
let currentPath = url.removeTrailingSlash(state.route.path);
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
if (modifierKeys) {
@@ -659,6 +662,10 @@ export default {
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
// Allow native browser navigation when Alt is held
if (event.altKey) {
return;
}
event.preventDefault();
this.navigateKeboardArrows(key);
break;
@@ -918,7 +925,7 @@ export default {
// if control or shift is pressed, do not clear the selection
if (this.ctrKeyPressed || event.shiftKey) return;
const sameAsBefore = state.selected == this.lastSelected;
if (sameAsBefore && !state.multiple && getters.currentPromptName()) {
if (sameAsBefore && !state.multiple && getters.currentPromptName() == "") {
mutations.resetSelected();
}
this.lastSelected = state.selected;
+2 -2
View File
@@ -2,7 +2,7 @@
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<div class="preview" v-if="!isDeleted">
<ExtendedImage v-if="previewType == 'image' || pdfConvertable" :src="raw"
@navigate-previous="prev"
@navigate-previous="prev"
@navigate-next="next">
</ExtendedImage>
<audio v-else-if="previewType == 'audio'" ref="player" :src="raw" controls :autoplay="autoPlay"
@@ -128,7 +128,7 @@ export default {
raw() {
if (this.pdfConvertable) {
if (getters.isShare()) {
const previewPath = url.removeTrailingSlash(state.req.path) + "/" + this.name;
const previewPath = url.removeTrailingSlash(state.req.path);
return publicApi.getPreviewURL(previewPath,"original");
}
return (
+13 -2
View File
@@ -11,7 +11,8 @@
<th>{{ $t("general.hash") }}</th>
<th>{{ $t("settings.path") }}</th>
<th>{{ $t("settings.shareDuration") }}</th>
<th v-if="user.permissions.admin">{{ $t("settings.username") }}</th>
<th>{{ $t("settings.downloads") }}</th>
<th>{{ $t("settings.username") }}</th>
<th></th>
<th></th>
<th></th>
@@ -27,7 +28,11 @@
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t("permanent") }}</template>
</td>
<td v-if="user.permissions.admin">{{ link.username }}</td>
<td>
<template v-if="link.downloadsLimit && link.downloadsLimit > 0">{{ link.downloads }} / {{ link.downloadsLimit }}</template> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<template v-else>{{ link.downloads }}</template>
</td>
<td>{{ link.username }}</td>
<td class="small">
<button class="action" @click="editLink(link)" :aria-label="$t('buttons.edit')"
:title="$t('buttons.edit')">
@@ -151,3 +156,9 @@ export default {
},
};
</script>
<style scoped>
tr > td {
text-align: center;
}
</style>
@@ -6,7 +6,7 @@ test("navigate folders", async ({ page, checkForErrors, context }) => {
// excluded folder should not be visible in the file list
await expect(page.locator('a[aria-label="excluded"]')).toHaveCount(0);
await page.goto("/files/excluded");
const msg = "Error: {\"status\":500,\"message\":\"could not refresh file info: directory or item excluded from indexing\"}"
const msg = "500: could not refresh file info: directory or item excluded from indexing"
await expect(page.locator('#popup-notification-content')).toHaveText(msg);
checkForErrors(2,1); // expect error not indexed
});
@@ -1,6 +1,6 @@
import { test, expect } from "../test-setup";
test("share folderbreadcrumbs navigation checks", async ({ page, checkForErrors, context }) => {
test("share folder breadcrumbs navigation checks", async ({ page, checkForErrors, context }) => {
await page.goto("/files/playwright%20+%20files/share");
await page.waitForSelector('#breadcrumbs');
let spanChildrenCount = await page.locator('#breadcrumbs > ul > li.item').count();
@@ -63,3 +63,44 @@ test("share download single file", async ({ page, checkForErrors, context }) =>
await expect(popup).toHaveText("Downloading...");
checkForErrors(0,1);
});
test("share private source", async ({ page, checkForErrors, openContextMenu }) => {
await page.goto("/files/docker");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - backend");
// Create a share of folder
await openContextMenu();
await page.locator('button[aria-label="Share"]').click();
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /');
await page.locator('button[aria-label="Share-Confirm"]').click();
await expect(page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th))")).toHaveCount(0);
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("403: the target source is private, sharing is not permitted");
checkForErrors(1,1); // 1 error is expected for the private source
});
test("share file creation", async ({ page, checkForErrors, openContextMenu }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHashShare = await page.evaluate(() => localStorage.getItem('shareHashShare'));
if (shareHashShare == "") {
throw new Error("Share hash not found in localStorage");
}
await page.goto("public/share/" + shareHashShare);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - share");
await openContextMenu();
// await page.locator('button[aria-label="New file"]').click();
// await page.locator('input[aria-label="FileName Field"]').waitFor({ state: 'visible' });
// await page.locator('input[aria-label="FileName Field"]').fill('dfsaf.txt');
// await page.locator('button[aria-label="Create"]').click();
// await expect(page).toHaveTitle("Graham's Filebrowser - Files - dfsaf.txt");
// await page.locator(".ace_content").click();
// await page.keyboard.type("test content");
// await page.locator(".overflow-menu-button").click();
// await page.locator('button[aria-label="Save"]').click();
// const popup = page.locator('#popup-notification-content');
// await popup.waitFor({ state: 'visible' });
// await expect(popup).toHaveText("dfsaf.txt saved successfully.");
checkForErrors();
});