Beta/v0.8.1 (#1106)
This commit is contained in:
1 parent
7d0707ebe0
commit
5b0f388fa5
78 files changed
+1856
-618
No files matched your search
@@ -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:
|
||||
|
||||
@@ -11,6 +11,7 @@ server:
|
||||
- path: "."
|
||||
name: "docker"
|
||||
config:
|
||||
private: true
|
||||
defaultEnabled: true
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == "\\" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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: []
|
||||
|
||||
@@ -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 || {}),
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
return getters.multibuttonState();
|
||||
},
|
||||
stickSidebar() {
|
||||
return state.stickSidebar;
|
||||
return state.user?.stickySidebar;
|
||||
},
|
||||
currentView() {
|
||||
return getters.currentView();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<button
|
||||
v-for="(btn, index) in buttons"
|
||||
:key="index"
|
||||
class="clickable"
|
||||
:class="{ active: activeButton === index }"
|
||||
@click="setActiveButton(index, btn.value)"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'))"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
width: 100%;
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.input:hover,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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": "قواعد الوصول",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Κανόνες πρόσβασης",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "כללי גישה",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "アクセス規則",
|
||||
|
||||
@@ -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": "액세스 규칙",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Правила доступа",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ı",
|
||||
|
||||
@@ -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": "Правила доступу",
|
||||
|
||||
@@ -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": "访问规则",
|
||||
|
||||
@@ -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": "存取規則",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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...`
|
||||
|
||||
@@ -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,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 (
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in new issue
Block a user