Compare commits

...
11 Commits
57 changed files with 930 additions and 1580 deletions

No files matched your search

+18
View File
@@ -2,6 +2,24 @@
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).
## v1.2.2-stable
**Security**:
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse)
- Patched Incomplete Remediation of CVE-2026-27611: Password-Protected Share Bypass via /public/api/share/info GHSA-525j-95gf-766f
**New Features**:
- "Divider" option in sidebar links to add a text or divider between links (#1875)
- shares offer a "go to source Location" sidebar link and button when editing a share.
**Notes**:
- Share edit/delete permissions are scoped to the user's shares rather than global (#2050)
- OIDC group claims accepted as map (#2084)
**BugFixes**:
- fixed the requirement that the database path needed to be set in the config file, now it loads `FILEBROWSER_DATABASE` value by default, fallback to config file property.
- Error downloading zipped directory: no such file or directory (users with scope) #2015
## v1.2.1-stable
**BugFixes**:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gtstef/ffmpeg:8.0.1-decode AS ffmpeg
FROM gtstef/ffmpeg:8.1-decode AS ffmpeg
FROM golang:alpine AS base
ARG VERSION
ARG REVISION
+12 -4
View File
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
)
@@ -42,14 +43,21 @@ func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.U
}
}
user, err := userStore.Get(username)
if err != nil {
return nil, fmt.Errorf("unable to get user from store: %v", err)
user, getErr := userStore.Get(username)
var passwordHash string
if getErr != nil {
passwordHash = utils.InvalidPasswordHash
} else {
passwordHash = user.Password
}
err = users.CheckPwd(password, user.Password)
// always run checkPwd to prevent timing attacks
err = utils.CheckPwd(password, passwordHash)
if err != nil {
return nil, err
}
if getErr != nil {
return nil, fmt.Errorf("unable to get user from store: %v", err)
}
// check for OTP for password
if user.TOTPSecret != "" {
+9 -4
View File
@@ -89,6 +89,11 @@ func StartFilebrowser() {
settings.Env.EmbeddedFs = os.IsNotExist(err)
}
err = utils.SetInvalidPasswordHash()
if err != nil {
logger.Fatalf("Failed to set security hash: %v", err)
}
sourceList := []string{}
for path, source := range settings.Config.Server.SourceMap {
sourceList = append(sourceList, fmt.Sprintf("%v: %v", source.Name, path))
@@ -182,7 +187,7 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
cacheDir := settings.Config.Server.CacheDir
numWorkers := settings.Config.Server.NumImageProcessors
ffmpeg.SetFFmpegPaths()
// Initialize asset filesystem before starting services
if settings.Env.EmbeddedFs {
embeddedAssets := fbhttp.GetEmbeddedAssets()
@@ -194,7 +199,7 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
} else {
fileutils.InitAssetFS(nil, false)
}
// Start preview service
err := preview.StartPreviewGenerator(numWorkers, cacheDir)
if err != nil {
@@ -202,12 +207,12 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
}
logger.Debugf("MuPDF Enabled : %v", settings.Env.MuPdfAvailable)
logger.Debugf("Media Enabled : %v", settings.MediaEnabled())
// Generate PWA icons after preview service is initialized
if err := icons.GeneratePWAIcons(); err != nil {
logger.Warningf("Failed to generate PWA icons: %v", err)
}
fbhttp.StartHttp(ctx, store, shutdownComplete)
return nil
}
+3 -8
View File
@@ -480,8 +480,8 @@ func loadConfigWithDefaults(configFile string, generate bool) error {
// Check if config file exists
if _, err := os.Stat(configFile); err != nil {
if configFile != "" {
logger.Errorf("could not open config file '%v', using default settings.", configFile)
if configFile != "" && configFile != "config.yaml" {
logger.Fatalf("could not open config file '%v'", configFile)
}
Config.Server.Sources = []*Source{
{
@@ -634,14 +634,9 @@ func setDefaults(generate bool) Settings {
database := os.Getenv("FILEBROWSER_DATABASE")
if database == "" {
database = "database.db"
} else {
// check if database file exists
if _, err := os.Stat(database); os.IsNotExist(err) {
database = "database.db"
}
}
if _, err := os.Stat(database); os.IsNotExist(err) {
logger.Warning("database file could not be found. If this is unexpected, the default path is `./database.db`, but it can be configured in the config file under `server.database`.")
logger.Warning("database file could not be found. If this is unexpected, please set the FILEBROWSER_DATABASE environment variable to the correct path.")
}
s := Settings{
Server: Server{
+28
View File
@@ -0,0 +1,28 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
var BcryptCost = bcrypt.DefaultCost
var InvalidPasswordHash = ""
// HashPwd hashes a password.
func HashPwd(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
return string(bytes), err
}
// CheckPwd checks if a password is correct.
func CheckPwd(password, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
func SetInvalidPasswordHash() error {
passwordHash, err := HashPwd(InsecureRandomIdentifier(16))
if err != nil {
return err
}
InvalidPasswordHash = passwordHash
return nil
}
+65 -3
View File
@@ -2,10 +2,13 @@ package share
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
)
// IsSingleFileShare determines if this share is for a single file (not a directory).
@@ -144,10 +147,69 @@ func (l *Link) HasReachedUserLimit(username string) bool {
return count >= l.DownloadsLimit
}
func (l *Link) GetSourceName() (string, error) {
func (l *Link) GetSourceName() string {
sourceInfo, ok := settings.Config.Server.SourceMap[l.Source]
if !ok {
return "", fmt.Errorf("source not found")
return ""
}
return sourceInfo.Name, nil
return sourceInfo.Name
}
func (l *Link) UserCanEdit(user *users.User) bool {
return l.UserID == user.ID || user.Permissions.Admin
}
func (l *Link) SourceURL(user *users.User) string {
sourceName := l.GetSourceName()
// get user scope path from share
userScope, err := user.GetScopeForSourceName(sourceName)
if err != nil {
return ""
}
if !strings.HasPrefix(l.Path, userScope) {
return ""
}
scopedPath := strings.TrimPrefix(l.Path, userScope)
return "/" + filepath.Join("files", sourceName, scopedPath)
}
func (l *Link) BannerURL() string {
_, _, err := l.GetShareImagePartsHelper(true)
if err == nil {
return fmt.Sprintf("%s%spublic/api/share/image?banner=true&hash=%s", settings.Config.Server.ExternalUrl, settings.Config.Server.BaseURL, l.Hash)
}
return l.Banner
}
func (l *Link) FaviconURL() string {
_, _, err := l.GetShareImagePartsHelper(false)
if err == nil {
return fmt.Sprintf("%s%spublic/api/share/image?favicon=true&hash=%s", settings.Config.Server.ExternalUrl, settings.Config.Server.BaseURL, l.Hash)
}
return l.Favicon
}
func (l *Link) GetShareImagePartsHelper(isBanner bool) (string, string, error) {
// Get the asset query string from share
var assetQueryString string
if isBanner {
assetQueryString = l.Banner
} else {
assetQueryString = l.Favicon
}
if assetQueryString == "" {
return "", "", fmt.Errorf("asset not configured for this share")
}
// Parse the query string to extract source and path
assetParams, err := url.ParseQuery(assetQueryString)
if err != nil {
return "", "", fmt.Errorf("invalid asset configuration: %v", err)
}
sourceName := assetParams.Get("source")
assetPath := assetParams.Get("path")
return sourceName, assetPath, nil
}
+2
View File
@@ -45,6 +45,8 @@ type CommonShare struct {
HasPassword bool `json:"hasPassword,omitempty"`
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
DisableLoginOption bool `json:"disableLoginOption"` // disable login option in share (true = hide, false = show)
SourceURL string `json:"sourceURL,omitempty"`
CanEdit bool `json:"canEdit,omitempty"`
}
type CreateBody struct {
CommonShare
+1
View File
@@ -18,6 +18,7 @@ import (
var userStore *users.Storage
func InitializeDb(path string) (*bolt.BoltStore, bool, error) {
logger.Debugf("Initializing database: %v", path)
exists, err := dbExists(path)
if err != nil {
panic(err)
+26 -26
View File
@@ -15,23 +15,23 @@ require (
github.com/google/go-cmp v0.7.0
github.com/gtsteffaniak/go-cache v1.1.0
github.com/gtsteffaniak/go-logger v1.0.2
github.com/kovidgoyal/imaging v1.8.19
github.com/mattn/go-sqlite3 v1.14.34
github.com/kovidgoyal/imaging v1.8.20
github.com/mattn/go-sqlite3 v1.14.37
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.48.0
golang.org/x/image v0.36.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.50.0
golang.org/x/oauth2 v0.35.0
golang.org/x/sys v0.41.0
golang.org/x/time v0.14.0
golang.org/x/tools v0.42.0
golang.org/x/crypto v0.49.0
golang.org/x/image v0.37.0
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.42.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.43.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.1
modernc.org/sqlite v1.47.0
)
require (
@@ -99,7 +99,7 @@ require (
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0-alpha.4 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
@@ -111,16 +111,16 @@ require (
github.com/go-critic/go-critic v0.14.3 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
@@ -136,7 +136,7 @@ require (
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang/geo v0.0.0-20260129164528-943061e2742c // indirect
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -272,13 +272,13 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.7.0 // indirect
modernc.org/libc v1.68.0 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
+62 -62
View File
@@ -244,8 +244,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.0-alpha.4 h1:JzPbdf+cqbyT9sZtP4xnqelwUXwf7LvD8xKS6+ofTds=
github.com/ebitengine/purego v0.10.0-alpha.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -298,33 +298,33 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -393,8 +393,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260129164528-943061e2742c h1:ysO2h2Odnl1AJM1I2Lm/fa6JvO0pECMSt2CwBaa+ITo=
github.com/golang/geo v0.0.0-20260129164528-943061e2742c/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a h1:7f/cr+n7Jtd7mzCLrx47sdVIGabpSSxCHrf6fJczDp0=
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -560,8 +560,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
github.com/kovidgoyal/imaging v1.8.20 h1:74GZ7C2rIm3rqmGEjK1GvvPOOnJ0SS5iDOa6Flfo0b0=
github.com/kovidgoyal/imaging v1.8.20/go.mod h1:d3phGYkTChGYkY4y++IjpHgUGhWGELDc2NEQAqxwZZg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -624,8 +624,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc=
@@ -908,8 +908,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -928,8 +928,8 @@ golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjO
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -955,8 +955,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -999,16 +999,16 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1024,8 +1024,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1078,8 +1078,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -1096,13 +1096,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1153,8 +1153,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -1273,18 +1273,18 @@ honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -1293,8 +1293,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+1 -1
View File
@@ -6,10 +6,10 @@ import (
"embed"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net"
"net/http"
"text/template"
"time"
_ "net/http/pprof"
+3
View File
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/url"
"slices"
@@ -44,6 +45,8 @@ func (u *userInfoUnmarshaller) UnmarshalJSON(data []byte) error {
if u.groupsClaim != "" {
if v, ok := raw[u.groupsClaim]; ok {
switch val := v.(type) {
case map[string]interface{}:
u.userInfo.Groups = slices.Collect(maps.Keys(val))
case []interface{}:
groups := make([]string, len(val))
for i, g := range val {
+8 -31
View File
@@ -339,7 +339,13 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
// Extract clean parameters from query string
source := r.URL.Query().Get("source")
path := r.URL.Query().Get("path")
user := d.user
if d.share != nil {
source = d.share.GetSourceName()
path = d.IndexPath
user = d.shareUser
}
// Validate required parameters
if (path == "" || source == "") && d.fileInfo.Hash == "" {
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
@@ -354,35 +360,6 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
}
path = cleanPath
var sourceInfo *settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: share source not found")
return returnOnlyOfficeError(w, r, 404, "source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return returnOnlyOfficeError(w, r, 404, "source not found")
}
}
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
userScope, scopeErr := d.user.GetScopeForSourceName(source)
if scopeErr != nil {
logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return returnOnlyOfficeError(w, r, 403, "source not available")
}
path = utils.JoinPathAsUnix(userScope, path)
} else {
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
}
// Handle document closure - clean up document key cache
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
@@ -487,7 +464,7 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
}
// Check share permissions first if this is a share request
if d.fileInfo.Hash != "" {
if d.share != nil {
if !d.share.AllowModify {
logger.Errorf("OnlyOffice callback: edit permission not allowed for this share")
return returnOnlyOfficeError(w, r, 403, "edit permission not allowed for this share")
@@ -528,7 +505,7 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
_, err = files.FileInfoFaster(utils.FileOptions{
Source: source,
Path: path,
}, store.Access, d.user)
}, store.Access, user)
if err != nil {
logger.Errorf("OnlyOffice callback: original file no longer exists at path=%s: %v",
path, err)
+8 -36
View File
@@ -4,14 +4,12 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
"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/preview"
"github.com/gtsteffaniak/go-logger/logger"
@@ -246,8 +244,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if !d.share.AllowModify {
return http.StatusForbidden, fmt.Errorf("create is not allowed for this share")
}
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
@@ -264,7 +262,7 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
resolvedPath := utils.JoinPathAsUnix(d.share.Path, cleanPath)
err = files.WriteFile(source, resolvedPath, r.Body)
err = files.WriteFile(sourceName, resolvedPath, r.Body)
// hide the error
if err != nil {
logger.Errorf("public put handler: error updating resource with error %v", err)
@@ -344,8 +342,8 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
}
// Get the source from the share
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
@@ -371,9 +369,9 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// Note: Share paths are absolute, so we don't strip user scope here
// resourcePatchHandler will skip adding scope for shares
for i := range req.Items {
req.Items[i].FromSource = source
req.Items[i].FromSource = sourceName
req.Items[i].FromPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].FromPath)
req.Items[i].ToSource = source
req.Items[i].ToSource = sourceName
req.Items[i].ToPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].ToPath)
}
d.Data = req
@@ -419,7 +417,7 @@ func getShareImage(w http.ResponseWriter, r *http.Request, d *requestContext) (i
return http.StatusNotFound, fmt.Errorf("user for share no longer exists")
}
sourceName, assetPath, err := getShareImagePartsHelper(d.share, isBanner)
sourceName, assetPath, err := d.share.GetShareImagePartsHelper(isBanner)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid asset configuration: %v", err)
}
@@ -464,29 +462,3 @@ func getShareImage(w http.ResponseWriter, r *http.Request, d *requestContext) (i
return status, err
}
func getShareImagePartsHelper(share *share.Link, isBanner bool) (string, string, error) {
// Get the asset query string from share
var assetQueryString string
if isBanner {
assetQueryString = share.Banner
} else {
assetQueryString = share.Favicon
}
if assetQueryString == "" {
return "", "", fmt.Errorf("asset not configured for this share")
}
// Parse the query string to extract source and path
assetParams, err := url.ParseQuery(assetQueryString)
if err != nil {
return "", "", fmt.Errorf("invalid asset configuration: %v", err)
}
sourceName := assetParams.Get("source")
assetPath := assetParams.Get("path")
return sourceName, assetPath, nil
}
+14 -34
View File
@@ -14,7 +14,6 @@ import (
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/indexing"
@@ -143,16 +142,6 @@ func addFile(source string, path string, d *requestContext, tarWriter *tar.Write
}
}
// Verify file exists
_, err := files.FileInfoFaster(utils.FileOptions{
Path: path,
Source: source,
Expand: false,
FollowSymlinks: true,
}, store.Access, d.user)
if err != nil {
return err
}
realPath, _, _ := idx.GetRealPath(path)
info, err := os.Stat(realPath)
if err != nil {
@@ -298,6 +287,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
var documentId string
var logContext *OnlyOfficeLogContext
// modify all filepaths for user scope
if d.share == nil {
userscope, err = d.user.GetScopeForSourceName(source)
if err != nil {
@@ -319,8 +309,11 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
}
return http.StatusForbidden, err
}
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
for i, filePath := range fileList {
fileList[i] = utils.JoinPathAsUnix(userscope, filePath)
}
}
firstFilePath = fileList[0]
// For shares, the path is already correctly resolved by publicRawHandler
idx := indexing.GetIndex(source)
if idx == nil {
@@ -505,7 +498,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
_, err = io.Copy(w, reader)
os.Remove(archiveData) // Remove the file after streaming
if err != nil {
logger.Errorf("Failed to copy archive data to response: %v", err)
logger.Debugf("client likely aborted: %v, failed to copy archive data to response: %v", r.Context().Err(), err)
return http.StatusInternalServerError, err
}
@@ -519,30 +512,17 @@ func computeArchiveSize(source string, fileList []string, d *requestContext) (in
return 0, fmt.Errorf("source %s is not available", source)
}
var userScope string
var err error
if d.share == nil {
userScope, err = d.user.GetScopeForSourceName(source)
if err != nil {
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
}
}
for _, path := range fileList {
var fullPath string
if d.share == nil {
fullPath = utils.JoinPathAsUnix(userScope, path)
// Check access control for each file in the archive
// Silently skip if access is denied (as if the file doesn't exist)
if store.Access != nil && !store.Access.Permitted(idx.Path, fullPath, d.user.Username) {
if store.Access != nil && !store.Access.Permitted(idx.Path, path, d.user.Username) {
continue // Skip this file and continue with the next one
}
} else {
fullPath = path
}
// For shares, the path is already correctly resolved by publicRawHandler
realPath, isDir, err := idx.GetRealPath(fullPath)
realPath, isDir, err := idx.GetRealPath(path)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -568,11 +548,11 @@ func createZip(d *requestContext, source string, tmpDirPath string, filenames ..
zipWriter := zip.NewWriter(file)
for _, fname := range filenames {
err := addFile(source, fname, d, nil, zipWriter, false)
for _, filepath := range filenames {
err := addFile(source, filepath, d, nil, zipWriter, false)
if err != nil {
// Access control failures return nil, so any error here is a real error
logger.Errorf("Failed to add %s to ZIP: %v", fname, err)
logger.Errorf("Failed to add %s to ZIP: %v", filepath, err)
return err
}
}
@@ -595,10 +575,10 @@ func createTarGz(d *requestContext, source string, tmpDirPath string, filenames
gzWriter := gzip.NewWriter(file)
tarWriter := tar.NewWriter(gzWriter)
for _, fname := range filenames {
err := addFile(source, fname, d, tarWriter, nil, false)
for _, filepath := range filenames {
err := addFile(source, filepath, d, tarWriter, nil, false)
if err != nil {
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
return err
}
}
+5 -5
View File
@@ -252,12 +252,12 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
}
if d.share != nil {
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
// get user scope path from share
userScope, err := d.shareUser.GetScopeForSourceName(source)
userScope, err := d.shareUser.GetScopeForSourceName(sourceName)
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
@@ -272,7 +272,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
FollowSymlinks: true,
Path: indexPath,
Source: source,
Source: sourceName,
ShowHidden: true,
}, store.Access, filePermUser)
if err != nil {
@@ -280,7 +280,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
}
// Delete the file/directory
err = files.DeleteFiles(source, fileInfo.RealPath, fileInfo.Type == "directory")
err = files.DeleteFiles(sourceName, fileInfo.RealPath, fileInfo.Type == "directory")
if err != nil {
logger.Errorf("resource bulk delete handler: error deleting file/directory: %v", err)
response.Failed = append(response.Failed, BulkDeleteItem{
+44 -17
View File
@@ -33,13 +33,14 @@ type ShareResponse struct {
}
// convertToFrontendShareResponse converts shares to response format with usernames
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link, user *users.User) ([]*ShareResponse, error) {
responses := make([]*ShareResponse, 0, len(shares))
for _, s := range shares {
user, err := store.Users.Get(s.UserID)
// Look for the username of the user who created the share
creator, err := store.Users.Get(s.UserID)
username := ""
if err == nil {
username = user.Username
username = creator.Username
}
// Get source info to convert path to name for frontend
@@ -65,6 +66,9 @@ func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*S
s.CommonShare.HasPassword = s.HasPassword()
s.DownloadURL = getShareURL(r, s.Hash, true, s.Token)
s.ShareURL = getShareURL(r, s.Hash, false, s.Token)
if s.UserCanEdit(user) {
s.CommonShare.SourceURL = s.SourceURL(user)
}
// Create response with source name (overrides the embedded Link's source field)
responses = append(responses, &ShareResponse{
Link: s,
@@ -97,7 +101,7 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
return http.StatusInternalServerError, err
}
shares = utils.NonNilSlice(shares)
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares)
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -137,7 +141,7 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
}
sharesWithUsernames, err := convertToFrontendShareResponse(r, s)
sharesWithUsernames, err := convertToFrontendShareResponse(r, s, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -162,7 +166,16 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
return http.StatusBadRequest, nil
}
err := store.Share.Delete(hash)
// only allow users to delete their own shares
thisShare, err := store.Share.GetByHash(hash)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("share not found")
}
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
return http.StatusForbidden, fmt.Errorf("you are not allowed to delete this share")
}
err = store.Share.Delete(hash)
if err != nil {
return errToStatus(err), err
}
@@ -196,8 +209,16 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
return http.StatusBadRequest, fmt.Errorf("hash and path are required")
}
// only allow users to update their own shares
thisShare, err := store.Share.GetByHash(body.Hash)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("share not found")
}
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
return http.StatusForbidden, fmt.Errorf("you are not allowed to update this share")
}
// Update the share path
err := store.Share.UpdateSharePath(body.Hash, body.Path)
err = store.Share.UpdateSharePath(body.Hash, body.Path)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -209,7 +230,7 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
}
// Convert to response format
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare})
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare}, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -389,7 +410,7 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err = store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s})
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s}, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -611,16 +632,11 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
commonShare := shareLink.CommonShare
commonShare.ShareURL = getShareURL(r, hash, false, "")
_, _, err = getShareImagePartsHelper(shareLink, true)
if err == nil {
commonShare.BannerUrl = fmt.Sprintf("%spublic/api/share/image?banner=true&hash=%s", config.Server.BaseURL, hash)
}
_, _, err = getShareImagePartsHelper(shareLink, false)
if err == nil {
commonShare.FaviconUrl = fmt.Sprintf("%spublic/api/share/image?favicon=true&hash=%s", config.Server.BaseURL, hash)
}
commonShare.BannerUrl = shareLink.BannerURL()
commonShare.FaviconUrl = shareLink.FaviconURL()
commonShare.Source = ""
commonShare.Path = ""
commonShare.DownloadURL = ""
commonShare.SidebarLinks = []users.SidebarLink{}
for _, link := range shareLink.SidebarLinks {
if link.Category == "download" && shareLink.ShareType == "upload" {
@@ -629,6 +645,17 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
}
}
commonShare.SourceURL = shareLink.SourceURL(d.user)
commonShare.CanEdit = shareLink.UserCanEdit(d.user)
if commonShare.SourceURL != "" {
link := users.SidebarLink{
Name: "sourceLocation",
Category: "custom",
Target: commonShare.SourceURL,
}
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
}
return renderJSON(w, r, commonShare)
}
+21 -24
View File
@@ -3,12 +3,12 @@ package http
import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
@@ -25,10 +25,18 @@ type TemplateRenderer struct {
// Render renders a template document with headers and data
func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}) error {
// If in dev mode, reload templates on every render.
templates := t.templates
// If in dev mode, reload templates on every render by creating a fresh instance
if t.devMode {
var err error
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
templates = template.New("").Funcs(template.FuncMap{
"marshal": func(v interface{}) (string, error) {
var a []byte
a, err = json.Marshal(v)
return string(a), err
},
})
templates, err = templates.ParseFS(assetFs, "public/index.html")
if err != nil {
return fmt.Errorf("error reloading template: %w", err)
}
@@ -39,7 +47,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
w.Header().Set("X-Accel-Expires", "0")
w.Header().Set("Transfer-Encoding", "identity")
// Execute the template with the provided data
return t.templates.ExecuteTemplate(w, name, data)
return templates.ExecuteTemplate(w, name, data)
}
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestContext, file, contentType string) (int, error) {
@@ -111,21 +119,10 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
if d.share.DisableSidebar {
disableSidebar = true
}
if strings.HasPrefix(strings.ToLower(d.share.Banner), "http") {
banner = d.share.Banner
} else {
_, _, err := getShareImagePartsHelper(d.share, true)
if err == nil {
banner = fmt.Sprintf("%s%spublic/api/share/image?banner=true&hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, d.share.Hash)
}
}
if strings.HasPrefix(strings.ToLower(d.share.Favicon), "http") {
favicon = d.share.Favicon
} else {
_, _, err := getShareImagePartsHelper(d.share, false)
if err == nil {
favicon = fmt.Sprintf("%s%spublic/api/share/image?favicon=true&hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, d.share.Hash)
}
banner = d.share.BannerURL()
shareFavicon := d.share.FaviconURL()
if shareFavicon != "" {
favicon = shareFavicon
}
shareHash = d.share.Hash
}
@@ -180,8 +177,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
data["htmlVars"] = map[string]interface{}{
"title": title,
"customCSS": config.Frontend.Styling.CustomCSSRaw,
"userSelectedTheme": userSelectedTheme,
"customCSS": template.CSS(config.Frontend.Styling.CustomCSSRaw),
"userSelectedTheme": template.CSS(userSelectedTheme),
"lightBackground": config.Frontend.Styling.LightBackground,
"darkBackground": config.Frontend.Styling.DarkBackground,
"staticURL": staticURL,
@@ -192,7 +189,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"winIcon": staticURL + "/mstile-256x256.png",
"appIcon": staticURL + "/apple-touch-icon.png",
"description": description,
"loadingSpinnersCSS": loadingSpinnersCSS,
"loadingSpinnersCSS": template.CSS(loadingSpinnersCSS),
"banner": banner,
"image": ogImage,
"url": fullURL,
@@ -238,8 +235,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
return http.StatusInternalServerError, fmt.Errorf("error marshaling globalVars: %w", err)
}
// Replace with JSON strings for direct template usage
data["globalVars"] = string(globalVarsJSON)
// Mark as JS for html/template to avoid escaping
data["globalVars"] = template.JS(globalVarsJSON)
// Render the template with global variables
if err := templateRenderer.Render(w, file, data); err != nil {
+2 -1
View File
@@ -196,7 +196,7 @@ func TestProcessDirectoryItem(t *testing.T) {
},
}
for _, tt := range tests {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
itemInfo, size, shouldCount := idx.processDirectoryItem(
tt.file, tt.combinedPath,
@@ -212,6 +212,7 @@ func TestProcessDirectoryItem(t *testing.T) {
if itemInfo == nil {
t.Fatalf("expected non-nil itemInfo")
return
}
if size != tt.expectSize {
+23 -5
View File
@@ -11,6 +11,7 @@ import (
"os"
"runtime"
"strings"
"time"
"github.com/gen2brain/go-fitz"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
@@ -32,10 +33,14 @@ func (s *Service) GenerateImageFromDoc(ctx context.Context, file iteminfo.Extend
s.docGenMutex.Lock()
defer s.docGenMutex.Unlock()
// 2. Lock the current goroutine to a single OS thread for CGo calls
// Lock the current goroutine to a single OS thread for CGo calls
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Create a 2-second timeout for document generation after acquiring locks
timeoutCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
docPath := file.RealPath
// copy file to a temporary location if needed
if strings.HasPrefix(file.Type, "text") && !strings.HasSuffix(file.RealPath, ".txt") {
@@ -75,18 +80,26 @@ func (s *Service) GenerateImageFromDoc(ctx context.Context, file iteminfo.Extend
docPath = tempFilePath // Update docPath to point to the new temporary text snippet file
}
}
// Check timeout before opening document
if timeoutCtx.Err() != nil {
return nil, fmt.Errorf("document preview generation timed out after 2 seconds for '%s'", file.Name)
}
doc, err := fitz.New(docPath) // This calls the CGo version
if err != nil {
// The error message you received: "failed to open PDF: fitz: cannot open memory"
return nil, fmt.Errorf("failed to open PDF from memory for file '%s': %w", docPath, err)
}
defer doc.Close()
// Check timeout after opening document
if timeoutCtx.Err() != nil {
return nil, fmt.Errorf("document preview generation timed out after 2 seconds for '%s'", file.Name)
}
// Get the image from the doc page
// Ensure pageNumber is valid (e.g., >= 0 for 0-indexed or >= 1 for 1-indexed, check go-fitz docs)
// And pageNumber < doc.NumPage()
numPages := doc.NumPage()
if pageNumber < 0 || pageNumber >= numPages { // Assuming 0-indexed for Image()
if pageNumber < 0 || pageNumber >= numPages {
return nil, fmt.Errorf("invalid page number %d for PDF with %d pages ('%s')", pageNumber, numPages, docPath)
}
@@ -95,6 +108,11 @@ func (s *Service) GenerateImageFromDoc(ctx context.Context, file iteminfo.Extend
return nil, fmt.Errorf("failed to get image from page %d of '%s': %w", pageNumber, docPath, err)
}
// Check timeout after rendering image
if timeoutCtx.Err() != nil {
return nil, fmt.Errorf("document preview generation timed out after 2 seconds for '%s'", file.Name)
}
// Create a new buffer to hold the image bytes
var buf bytes.Buffer
+24
View File
@@ -3614,6 +3614,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -3725,6 +3728,9 @@ const docTemplate = `{
"description": "Override embedded field to show source name",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5062,6 +5068,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5158,6 +5167,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5200,6 +5212,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5305,6 +5320,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5350,6 +5368,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5458,6 +5479,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
+24
View File
@@ -3603,6 +3603,9 @@
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -3714,6 +3717,9 @@
"description": "Override embedded field to show source name",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5051,6 +5057,9 @@
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5147,6 +5156,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5189,6 +5201,9 @@
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5294,6 +5309,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5339,6 +5357,9 @@
"bannerUrl": {
"type": "string"
},
"canEdit": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5447,6 +5468,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
+16
View File
@@ -122,6 +122,8 @@ definitions:
type: string
bannerUrl:
type: string
canEdit:
type: boolean
description:
type: string
disableAnonymous:
@@ -199,6 +201,8 @@ definitions:
source:
description: Override embedded field to show source name
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1199,6 +1203,8 @@ definitions:
type: string
bannerUrl:
type: string
canEdit:
type: boolean
description:
type: string
disableAnonymous:
@@ -1266,6 +1272,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1296,6 +1304,8 @@ definitions:
type: string
bannerUrl:
type: string
canEdit:
type: boolean
description:
type: string
disableAnonymous:
@@ -1369,6 +1379,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1401,6 +1413,8 @@ definitions:
type: string
bannerUrl:
type: string
canEdit:
type: boolean
description:
type: string
disableAnonymous:
@@ -1476,6 +1490,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
+254 -1206
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -27,7 +27,6 @@
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
"file-loader": "^6.2.0",
"glob": "^9.3.5",
"highlight.js": "^11.11.1",
"mammoth": "^1.9.1",
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{{ .htmlVars.title }}</title>
<title>"{{ .htmlVars.title }}"</title>
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
+9 -1
View File
@@ -34,8 +34,13 @@
</div>
<div v-else-if="!isPickingFile">
<div aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
<div v-if="isFiles" aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
displayPath }}</div>
<div v-else>
<a :href="link.sourceURL" class="button button--block">
{{ $t('buttons.goToSource') }}
</a>
</div>
<p> {{ $t('share.notice') }} </p>
<div v-if="listing">
@@ -474,6 +479,9 @@ export default {
isListing() {
return getters.isListing(); // Access getter directly from the store
},
isFiles() {
return getters.isFiles(); // Access getter directly from the store
},
url() {
if (state.isSearchActive) {
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
@@ -20,7 +20,10 @@
class="link-drag-handle">
<i class="material-icons">drag_indicator</i>
</div>
<div class="link-icon">
<div v-if="link.category === 'divider'" class="link-icon">
<i class="material-icons">horizontal_rule</i>
</div>
<div v-else class="link-icon">
<i :class="getIconClass(link.icon)">{{ link.icon }}</i>
</div>
<div class="link-details">
@@ -66,6 +69,7 @@
<option value="share">{{ $t('general.share') }}</option>
<option v-if="context === 'user'" value="tool">{{ $t('general.tool') }}</option>
<option value="custom">{{ $t('sidebar.customLink') }}</option>
<option value="divider">{{ $t('general.divider') }}</option>
<option v-if="context === 'share'" value="shareInfo">{{ $t('share.shareInfo') }}</option>
<option v-if="context === 'share'" value="download">{{ $t('general.download') }}</option>
</select>
@@ -148,6 +152,13 @@
<p>{{ $t('share.downloadDescription') }}</p>
</div>
<!-- Divider - special visual separator -->
<div v-if="newLink.category === 'divider'" class="form-group">
<p>{{ $t('sidebar.linkName') }}</p>
<input aria-label="Link Name" v-model="newLink.name" type="text" class="input"
:placeholder="$t('sidebar.dividerNamePlaceholder')" />
</div>
<!-- Custom Link Input -->
<div v-if="newLink.category === 'custom'" class="form-group">
<p>{{ $t('sidebar.linkName') }}</p>
@@ -159,8 +170,8 @@
:placeholder="$t('sidebar.linkUrlPlaceholder')" />
</div>
<!-- Icon Selection - Available for ALL link types -->
<div v-if="newLink.category" class="form-group">
<!-- Icon Selection - Available for ALL link types except divider -->
<div v-if="newLink.category && newLink.category !== 'divider'" class="form-group">
<p>{{ $t('sidebar.linkIcon') }}</p>
<div class="icon-input-group">
<input v-model="newLink.icon" type="text" class="input icon-input"
@@ -285,8 +296,10 @@ export default {
isNewLinkValid() {
if (!this.newLink.category) return false;
// Special link types for shares don't need additional validation
if (this.newLink.category === "shareInfo" || this.newLink.category === "download") {
// Special link types that don't need additional validation
if (this.newLink.category === "shareInfo" ||
this.newLink.category === "download" ||
this.newLink.category === "divider") {
return true;
}
@@ -392,10 +405,16 @@ export default {
share: this.$t('general.share'),
shareInfo: this.$t('share.shareInfo'),
download: this.$t('general.download'),
divider: this.$t('general.divider'),
};
return labels[category] || category;
},
getLinkDisplayName(link) {
// For dividers, show the name or a default
if (link.category === 'divider') {
return link.name || this.$t('general.divider');
}
// Check if the name looks like a translation key that needs translating
if (link.category === 'shareInfo' && link.name === 'share.shareInfo') {
return this.$t('share.shareInfo');
@@ -546,6 +565,11 @@ export default {
} else if (this.newLink.category === "download") {
// Download is a special action link
linkData.target = "#";
} else if (this.newLink.category === "divider") {
// Divider is a visual separator with no action
linkData.target = "#";
linkData.name = this.newLink.name || ""; // Keep the custom name if provided
linkData.icon = ""; // Dividers don't need an icon
} else if (this.newLink.category === "custom") {
linkData.target = this.processCustomUrl(this.newLink.target);
} else if (this.newLink.category === "source") {
+64 -26
View File
@@ -7,17 +7,33 @@
</div>
<!-- Links section header -->
<div class="sidebar-links-header" :class="{ 'no-edit-options': isShare, 'with-top-spacing': isShare && !disableShareCard }">
<i v-if="!isShare" @click="goHome()" class="material-icons action">home</i>
<div class="sidebar-links-header"
:class="{ 'with-top-spacing': isShare && !disableShareCard }">
<i :class="{ 'disabled': !isLoggedIn }" @click="goHome()" class="material-icons action">home</i>
<span>{{ $t("general.links") }}</span>
<i v-if="!isShare" @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
<i v-if="isShare" aria-label="Edit Share" @mouseenter="showTooltip($event, editShareText())" @mouseleave="hideTooltip"
:class="{ 'disabled': !canEdit }"
@click="showEditShareHover" class="material-icons action">edit</i>
<i v-else @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
@click="openSidebarLinksPrompt" class="material-icons action">edit</i>
</div>
<transition-group name="expand" tag="div" class="inner-card">
<template v-for="(link, index) in sidebarLinksToDisplay" :key="`link-${index}-${link.category}`">
<!-- Divider: renders as hr line or text label -->
<div v-if="link.category === 'divider'" class="sidebar-divider-container">
<hr v-if="!link.name || link.name.toLowerCase() === 'divider'" class="sidebar-divider" />
<span v-else class="sidebar-divider-text">{{ link.name }}</span>
</div>
<a v-else-if="link.category == 'custom' && link.name == 'sourceLocation'" :href="link.target"
:aria-label="link.name" class="action button sidebar-link-button" @click.prevent="handleLinkClick(link)">
<div class="link-container">
<i class="material-icons link-icon">open_in_new</i>
<span>{{ $t('buttons.goToSource') }}</span>
</div>
</a>
<!-- Source-type links: styled exactly like original sources -->
<a v-if="link.category === 'source'" :href="getLinkHref(link)"
<a v-else-if="link.category === 'source'" :href="getLinkHref(link)"
class="action button source-button sidebar-link-button" :class="{
active: isLinkActive(link),
disabled: !isLinkAccessible(link)
@@ -46,12 +62,11 @@
</i>
</div>
<div v-if="hasUsageInfo(link)" class="usage-info">
<ProgressBar
<ProgressBar
:key="`progress-${link.sourceName}-${sourceInfo[link.sourceName]?.used || 0}-${sourceInfo[link.sourceName]?.total || 0}`"
:val="getProgressBarValue(sourceInfo[link.sourceName] || {})"
:max="(sourceInfo[link.sourceName] || {}).total || 1"
:status="getProgressBarStatus(sourceInfo[link.sourceName] || {})"
unit="bytes">
:val="getProgressBarValue(sourceInfo[link.sourceName] || {})"
:max="(sourceInfo[link.sourceName] || {}).total || 1"
:status="getProgressBarStatus(sourceInfo[link.sourceName] || {})" unit="bytes">
</ProgressBar>
</div>
</a>
@@ -59,22 +74,12 @@
<!-- Non-source links: tool and custom links with simple icon style -->
<a v-else :aria-label="link.name" :href="getLinkHref(link)" class="action button sidebar-link-button"
:class="{ active: isLinkActive(link) }" @click.prevent="handleLinkClick(link)">
<div class="link-container">
<div class="link-container">
<i :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
<span>{{ link.name }}</span>
</div>
</a>
</template>
<!-- Edit Share link - only shown when viewing a share and user has share permissions -->
<a v-if="isShare && canEditShare" :aria-label="$t('general.edit', { suffix: ' ' + $t('general.share') })" href="#"
class="action button sidebar-link-button"
@click.prevent="showEditShareHover">
<div class="link-container">
<i class="material-icons link-icon">edit</i>
<span>{{ $t("general.edit", { suffix: " " + $t("general.share") }) }}</span>
</div>
</a>
</transition-group>
</div>
</transition>
@@ -97,11 +102,17 @@ export default {
ShareInfo,
},
computed: {
canEdit() {
return state.shareInfo?.canEdit || false;
},
isLoggedIn() {
return state.user?.username !== 'anonymous';
},
isShare: () => getters.isShare(),
hasLinks() {
return this.sidebarLinksToDisplay?.length > 0;
},
user: () => (state.user || {username: 'anonymous'}),
user: () => (state.user || { username: 'anonymous' }),
sourceInfo() {
// Access state.sources.info to create reactive dependency
return state.sources.info;
@@ -117,10 +128,6 @@ export default {
// Check if user has customized their links
return this.user?.sidebarLinks && this.user.sidebarLinks.length > 0;
},
canEditShare() {
// Check if user is logged in and has share permissions
return state.user && state.user.permissions && state.user.permissions.share;
},
// Share info card props
disableShareCard() {
return state.shareInfo?.disableShareCard;
@@ -151,6 +158,9 @@ export default {
},
methods: {
getIconClass,
editShareText() {
return this.$t("general.edit") + " " + this.$t("general.share");
},
hasUsageInfo(link) {
// Check if usage info should be displayed for this link
// Returns true when link is accessible and has usage > 0
@@ -185,6 +195,9 @@ export default {
return baseURL + target;
},
goHome() {
if (!this.isLoggedIn) {
return;
}
this.$router.push('/');
},
getDefaultLinks() {
@@ -320,6 +333,9 @@ export default {
mutations.closeHovers();
},
openSidebarLinksPrompt() {
if (!this.isLoggedIn) {
return;
}
mutations.showHover({
name: "SidebarLinks",
});
@@ -407,12 +423,12 @@ export default {
</script>
<style scoped>
.no-edit-options {
justify-content: center !important;
}
.share-info-section {
margin-top: 0 !important;
margin-bottom: 1em;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--borderColor);
@@ -627,6 +643,7 @@ a.sidebar-link-button {
.realtime-pulse.warning>.center {
fill: rgb(255, 157, 0);
}
.vue-simple-progress {
margin-top: 0 !important;
}
@@ -640,4 +657,25 @@ a.sidebar-link-button {
.edit-share-button .link-icon {
color: var(--primaryColor);
}
.sidebar-divider-container {
width: 100%;
margin-top: 0.5em;
}
.sidebar-divider {
width: 50%;
border: none;
border-top: 0.1em solid var(--divider);
}
.sidebar-divider-text {
display: flex;
font-size: .85em;
color: var(--textSecondary);
letter-spacing: .05em;
justify-content: center;
align-content: center;
align-items: center;
}
</style>
+1
View File
@@ -8,6 +8,7 @@
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
transition: .1s ease all;
text-align: center;
}
.button--block {
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "الانتقال إلى العنصر",
"watchFile": "ملف المشاهدة",
"showMore": "إظهار الخيارات المتقدمة",
"showLess": "إخفاء الخيارات المتقدمة"
"showLess": "إخفاء الخيارات المتقدمة",
"goToSource": "الانتقال إلى موقع المصدر"
},
"download": {
"downloadFile": "Download File",
@@ -371,7 +372,8 @@
"pickIconDescription": "بحث وتحديد أيقونة من مكتبة الأيقونات المادية",
"shareLinksUpdatedSuccess": "تم تحديث روابط الشريط الجانبي المشاركة بنجاح!",
"shareHome": "مشاركة الصفحة الرئيسية",
"linkIconPlaceholder": "تحديد اسم رمز المادة اختياريًا"
"linkIconPlaceholder": "تحديد اسم رمز المادة اختياريًا",
"dividerNamePlaceholder": "نص المقسم (اتركه فارغًا لإظهار سطر)"
},
"success": {
"linkCopied": "تم نسخ الملف"
@@ -627,7 +629,8 @@
"lastUpdate": "آخر تحديث{suffix}",
"latency": "الكمون{suffix}",
"all": "الكل{suffix}",
"expiration": "انتهاء الصلاحية{suffix}"
"expiration": "انتهاء الصلاحية{suffix}",
"divider": "مقسم{suffix}"
},
"access": {
"rules": "قواعد الوصول",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Přejít na položku",
"watchFile": "Sledovat soubor",
"showMore": "Zobrazit rozšířené možnosti",
"showLess": "Skrytí pokročilých možností"
"showLess": "Skrytí pokročilých možností",
"goToSource": "Přejít na zdrojové umístění"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -508,7 +509,8 @@
"pickIconDescription": "Vyhledejte a vyberte ikonu z knihovny Material Icons.",
"shareLinksUpdatedSuccess": "Odkazy na sdílení postranního panelu byly úspěšně aktualizovány!",
"shareHome": "Sdílet Domů",
"linkIconPlaceholder": "Volitelně zadejte název ikony materiálu"
"linkIconPlaceholder": "Volitelně zadejte název ikony materiálu",
"dividerNamePlaceholder": "Text oddělovače (pro zobrazení řádku nechte prázdný)"
},
"success": {
"linkCopied": "Odkaz zkopírován!"
@@ -707,7 +709,8 @@
"lastUpdate": "Poslední aktualizace{suffix}",
"latency": "Zpoždění{suffix}",
"all": "Všechny{suffix}",
"expiration": "Expirace{suffix}"
"expiration": "Expirace{suffix}",
"divider": "Rozdělovač{suffix}"
},
"access": {
"rules": "Pravidla přístupu",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Weiter zu Artikel",
"watchFile": "Datei beobachten",
"showMore": "Erweiterte Optionen anzeigen",
"showLess": "Erweiterte Optionen ausblenden"
"showLess": "Erweiterte Optionen ausblenden",
"goToSource": "Zum Quellort gehen"
},
"download": {
"downloadFile": "Datei herunterladen",
@@ -371,7 +372,8 @@
"pickIconDescription": "Suchen und wählen Sie ein Symbol aus der Bibliothek der Materialsymbole",
"shareLinksUpdatedSuccess": "Links in der Seitenleiste erfolgreich aktualisiert!",
"shareHome": "Startseite teilen",
"linkIconPlaceholder": "Optional können Sie einen Materialsymbolnamen angeben"
"linkIconPlaceholder": "Optional können Sie einen Materialsymbolnamen angeben",
"dividerNamePlaceholder": "Teilertext (leer lassen, um eine Zeile anzuzeigen)"
},
"success": {
"linkCopied": "Link wurde kopiert!"
@@ -627,7 +629,8 @@
"lastUpdate": "Letzte Aktualisierung{suffix}",
"latency": "Latenzzeit{suffix}",
"all": "Alle{suffix}",
"expiration": "Verfallsdatum{suffix}"
"expiration": "Verfallsdatum{suffix}",
"divider": "Teiler{suffix}"
},
"access": {
"rules": "Zugangsregeln",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Πηγαίνετε στο στοιχείο",
"watchFile": "Παρακολουθήστε το αρχείο",
"showMore": "Εμφάνιση προηγμένων επιλογών",
"showLess": "Απόκρυψη προηγμένων επιλογών"
"showLess": "Απόκρυψη προηγμένων επιλογών",
"goToSource": "Πηγαίνετε στην τοποθεσία της πηγής"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -374,7 +375,8 @@
"pickIconDescription": "Αναζήτηση και επιλογή ενός εικονιδίου από τη βιβλιοθήκη Material Icons",
"shareLinksUpdatedSuccess": "Οι σύνδεσμοι της πλαϊνής μπάρας κοινής χρήσης ενημερώθηκαν με επιτυχία!",
"shareHome": "Μοιραστείτε το Home",
"linkIconPlaceholder": "Προαιρετικά καθορίστε ένα όνομα εικονιδίου υλικού"
"linkIconPlaceholder": "Προαιρετικά καθορίστε ένα όνομα εικονιδίου υλικού",
"dividerNamePlaceholder": "Κείμενο διαχωριστικού (αφήστε το κενό για να εμφανιστεί μια γραμμή)"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
@@ -627,7 +629,8 @@
"lastUpdate": "Τελευταία ενημέρωση{suffix}",
"latency": "Καθυστέρηση{suffix}",
"all": "Όλα{suffix}",
"expiration": "Λήξη{suffix}"
"expiration": "Λήξη{suffix}",
"divider": "Διαχωριστικό{suffix}"
},
"access": {
"rules": "Κανόνες πρόσβασης",
+4 -1
View File
@@ -8,6 +8,7 @@
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
},
"general": {
"divider": "Divider{suffix}",
"expiration": "Expiration{suffix}",
"all": "All{suffix}",
"name": "Name{suffix}",
@@ -150,7 +151,8 @@
"saveAndExit": "Save and Exit",
"keepEditing": "Keep Editing",
"closeAnyway": "Close Anyways",
"goToItem": "Go to Item"
"goToItem": "Go to Item",
"goToSource": "Go to Source Location"
},
"download": {
"downloadFile": "Download File",
@@ -592,6 +594,7 @@
"adminOptions": "Admin user options"
},
"sidebar": {
"dividerNamePlaceholder": "Divider text (leave blank to show a line)",
"siteSettings": "Site Settings",
"updateIsAvailable": "An update is available!",
"fileActions": "File actions",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Ir al elemento",
"watchFile": "Ver archivo",
"showMore": "Mostrar opciones avanzadas",
"showLess": "Ocultar opciones avanzadas"
"showLess": "Ocultar opciones avanzadas",
"goToSource": "Ir a la ubicación de origen"
},
"download": {
"downloadFile": "Descargar archivo",
@@ -371,7 +372,8 @@
"pickIconDescription": "Aquí puedes explorar los iconos más populates de la biblioteca \"Material Icons\"",
"shareLinksUpdatedSuccess": "¡Se actualizaron los accesos directos del recurso compartido!",
"shareHome": "Recurso compartido",
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste"
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste",
"dividerNamePlaceholder": "Texto divisor (déjelo en blanco para mostrar una línea)"
},
"success": {
"linkCopied": "¡Enlace copiado!"
@@ -627,7 +629,8 @@
"lastUpdate": "Última actualización{suffix}",
"latency": "Latencia{suffix}",
"all": "Todos{suffix}",
"expiration": "Caducidad{suffix}"
"expiration": "Caducidad{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Reglas de acceso",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Aller à l'article",
"watchFile": "Regarder le dossier",
"showMore": "Afficher les options avancées",
"showLess": "Masquer les options avancées"
"showLess": "Masquer les options avancées",
"goToSource": "Aller à l'emplacement de la source"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -371,7 +372,8 @@
"pickIconDescription": "Rechercher et sélectionner une icône dans la bibliothèque des icônes matérielles",
"shareLinksUpdatedSuccess": "Les liens de la barre latérale de partage ont été mis à jour avec succès !",
"shareHome": "Partager l'accueil",
"linkIconPlaceholder": "Spécifier éventuellement un nom d'icône de matériau"
"linkIconPlaceholder": "Spécifier éventuellement un nom d'icône de matériau",
"dividerNamePlaceholder": "Texte du séparateur (laisser en blanc pour montrer une ligne)"
},
"success": {
"linkCopied": "Lien copié !"
@@ -627,7 +629,8 @@
"lastUpdate": "Dernière mise à jour{suffix}",
"latency": "Temps de latence{suffix}",
"all": "Tous{suffix}",
"expiration": "Expiration{suffix}"
"expiration": "Expiration{suffix}",
"divider": "Diviseur{suffix}"
},
"access": {
"rules": "Règles d'accès",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "עבור לפריט",
"watchFile": "צפה בקובץ",
"showMore": "הצג אפשרויות מתקדמות",
"showLess": "הסתר אפשרויות מתקדמות"
"showLess": "הסתר אפשרויות מתקדמות",
"goToSource": "עבור למיקום המקור"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -371,7 +372,8 @@
"pickIconDescription": "חפש ובחר סמל מספריית Material Icons.",
"shareLinksUpdatedSuccess": "קישורי הסרגל הצדדי שותפו בהצלחה!",
"shareHome": "שתף בית",
"linkIconPlaceholder": "אפשר לציין שם סמל חומר"
"linkIconPlaceholder": "אפשר לציין שם סמל חומר",
"dividerNamePlaceholder": "טקסט מפריד (השאר ריק כדי להציג קו)"
},
"success": {
"linkCopied": "הקישור הועתק!"
@@ -627,7 +629,8 @@
"lastUpdate": "עדכון אחרון{suffix}",
"latency": "חביון{suffix}",
"all": "כל{suffix}",
"expiration": "תוקף{suffix}"
"expiration": "תוקף{suffix}",
"divider": "מפריד{suffix}"
},
"access": {
"rules": "כללי גישה",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Tovább a tételhez",
"watchFile": "Nézze meg a fájlt",
"showMore": "Speciális beállítások megjelenítése",
"showLess": "Speciális beállítások elrejtése"
"showLess": "Speciális beállítások elrejtése",
"goToSource": "Menj a forráshelyre"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -371,7 +372,8 @@
"pickIconDescription": "Keressen és válasszon ki egy ikont az Anyagi ikonok könyvtárból.",
"shareLinksUpdatedSuccess": "Megosztás oldalsáv linkek sikeresen frissítve!",
"shareHome": "Megosztás",
"linkIconPlaceholder": "Opcionálisan megadhatja az anyag ikon nevét"
"linkIconPlaceholder": "Opcionálisan megadhatja az anyag ikon nevét",
"dividerNamePlaceholder": "Választószöveg (üresen hagyva egy sor megjelenítéséhez)"
},
"success": {
"linkCopied": "Link másolva!"
@@ -627,7 +629,8 @@
"lastUpdate": "Utolsó frissítés{suffix}",
"latency": "Késleltetés{suffix}",
"all": "Minden{suffix}",
"expiration": "Lejárat{suffix}"
"expiration": "Lejárat{suffix}",
"divider": "Választó{suffix}"
},
"access": {
"rules": "Hozzáférési szabályok",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Vai all'articolo",
"watchFile": "Guarda il file",
"showMore": "Mostra opzioni avanzate",
"showLess": "Nascondere le opzioni avanzate"
"showLess": "Nascondere le opzioni avanzate",
"goToSource": "Vai alla posizione della fonte"
},
"download": {
"downloadFile": "Scarica file",
@@ -371,7 +372,8 @@
"pickIconDescription": "Cercare e selezionare un'icona dalla libreria Material Icons.",
"shareLinksUpdatedSuccess": "I link della barra laterale di condivisione sono stati aggiornati con successo!",
"shareHome": "Condividi Casa",
"linkIconPlaceholder": "Specificare facoltativamente il nome dell'icona del materiale"
"linkIconPlaceholder": "Specificare facoltativamente il nome dell'icona del materiale",
"dividerNamePlaceholder": "Testo del divisore (lasciare vuoto per mostrare una riga)"
},
"success": {
"linkCopied": "Link copiato!"
@@ -627,7 +629,8 @@
"lastUpdate": "Ultimo aggiornamento{suffix}",
"latency": "Latenza{suffix}",
"all": "Tutti{suffix}",
"expiration": "Scadenza{suffix}"
"expiration": "Scadenza{suffix}",
"divider": "Divisore{suffix}"
},
"access": {
"rules": "Regole di accesso",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "アイテムへ",
"watchFile": "ウォッチファイル",
"showMore": "詳細オプションを表示",
"showLess": "詳細オプションを隠す"
"showLess": "詳細オプションを隠す",
"goToSource": "ソース・ロケーションへ"
},
"download": {
"downloadFile": "ファイルをダウンロード",
@@ -371,7 +372,8 @@
"pickIconDescription": "マテリアル・アイコン・ライブラリからアイコンを検索して選択します。",
"shareLinksUpdatedSuccess": "サイドバーの共有リンクが正常に更新されました!",
"shareHome": "シェア・ホーム",
"linkIconPlaceholder": "オプションで素材アイコン名を指定"
"linkIconPlaceholder": "オプションで素材アイコン名を指定",
"dividerNamePlaceholder": "分割テキスト(行を表示するには空白にする)"
},
"success": {
"linkCopied": "リンクがコピーされました!"
@@ -627,7 +629,8 @@
"lastUpdate": "最終更新{suffix}",
"latency": "レイテンシー{suffix}",
"all": "すべて{suffix}",
"expiration": "有効期限{suffix}"
"expiration": "有効期限{suffix}",
"divider": "ディバイダー{suffix}"
},
"access": {
"rules": "アクセスルール",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "항목으로 이동",
"watchFile": "파일 보기",
"showMore": "고급 옵션 표시",
"showLess": "고급 옵션 숨기기"
"showLess": "고급 옵션 숨기기",
"goToSource": "소스 위치로 이동"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -371,7 +372,8 @@
"pickIconDescription": "머티리얼 아이콘 라이브러리에서 아이콘을 검색하고 선택합니다.",
"shareLinksUpdatedSuccess": "공유 사이드바 링크가 성공적으로 업데이트되었습니다!",
"shareHome": "홈 공유",
"linkIconPlaceholder": "선택적으로 머티리얼 아이콘 이름을 지정합니다."
"linkIconPlaceholder": "선택적으로 머티리얼 아이콘 이름을 지정합니다.",
"dividerNamePlaceholder": "디바이더 텍스트(줄을 표시하려면 비워둡니다)"
},
"success": {
"linkCopied": "링크가 복사되었습니다!"
@@ -627,7 +629,8 @@
"lastUpdate": "최근 업데이트{suffix}",
"latency": "지연 시간{suffix}",
"all": "모두{suffix}",
"expiration": "만료{suffix}"
"expiration": "만료{suffix}",
"divider": "디바이더{suffix}"
},
"access": {
"rules": "액세스 규칙",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Ga naar item",
"watchFile": "Bestand bekijken",
"showMore": "Geavanceerde opties weergeven",
"showLess": "Geavanceerde opties verbergen"
"showLess": "Geavanceerde opties verbergen",
"goToSource": "Ga naar de bronlocatie"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -371,7 +372,8 @@
"pickIconDescription": "Zoek en selecteer een pictogram uit de bibliotheek met materiaalpictogrammen",
"shareLinksUpdatedSuccess": "Zijbalk links delen succesvol bijgewerkt!",
"shareHome": "Thuis delen",
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op"
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op",
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
},
"success": {
"linkCopied": "Link gekopieerd!"
@@ -707,7 +709,8 @@
"lastUpdate": "Laatste update{suffix}",
"latency": "Latency{suffix}",
"all": "Alle{suffix}",
"expiration": "Vervaldatum{suffix}"
"expiration": "Vervaldatum{suffix}",
"divider": "Verdeler{suffix}"
},
"access": {
"rules": "Toegangsregels",
+7 -4
View File
@@ -116,7 +116,8 @@
"lines": "Regels{suffix}",
"lastUpdate": "Laatste update{suffix}",
"latency": "Vertraging{suffix}",
"expiration": "Vervaldatum{suffix}"
"expiration": "Vervaldatum{suffix}",
"divider": "Verdeler{suffix}"
},
"buttons": {
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
@@ -150,7 +151,8 @@
"saveAndExit": "Opslaan en afsluiten",
"keepEditing": "Blijven bewerken",
"closeAnyway": "Sluiten",
"goToItem": "Ga naar item"
"goToItem": "Ga naar item",
"goToSource": "Ga naar de bronlocatie"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -627,7 +629,8 @@
"linksUpdateFailed": "Bijwerken van koppelingen in zijbalk is mislukt",
"sourceNotAccessible": "Deze bron is niet langer toegankelijk voor jou",
"noLinksYet": "Er zijn nog geen links geconfigureerd. Voeg hieronder uw eerste link toe.",
"shareHome": "Thuis delen"
"shareHome": "Thuis delen",
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
},
"success": {
"linkCopied": "Koppeling gekopieerd!"
@@ -850,4 +853,4 @@
"reconnected": "Opnieuw verbinding gemaakt met server.",
"connectionLost": "De verbinding met de server is verbroken. Probeer opnieuw verbinding te maken..."
}
}
}
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Przejdź do pozycji",
"watchFile": "Oglądaj plik",
"showMore": "Pokaż opcje zaawansowane",
"showLess": "Ukryj opcje zaawansowane"
"showLess": "Ukryj opcje zaawansowane",
"goToSource": "Przejdź do lokalizacji źródłowej"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -371,7 +372,8 @@
"pickIconDescription": "Wyszukaj i wybierz ikonę z biblioteki Material Icons",
"shareLinksUpdatedSuccess": "Linki do paska bocznego zostały pomyślnie zaktualizowane!",
"shareHome": "Share Home",
"linkIconPlaceholder": "Opcjonalnie można określić nazwę ikony materiału"
"linkIconPlaceholder": "Opcjonalnie można określić nazwę ikony materiału",
"dividerNamePlaceholder": "Tekst podziału (pozostaw pusty, aby wyświetlić linię)"
},
"success": {
"linkCopied": "Link Skopiowany!"
@@ -627,7 +629,8 @@
"lastUpdate": "Ostatnia aktualizacja{suffix}",
"latency": "Opóźnienie{suffix}",
"all": "Wszystkie{suffix}",
"expiration": "Wygaśnięcie{suffix}"
"expiration": "Wygaśnięcie{suffix}",
"divider": "Dzielnik{suffix}"
},
"access": {
"rules": "Zasady dostępu",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Ir para o item",
"watchFile": "Assista ao arquivo",
"showMore": "Mostrar opções avançadas",
"showLess": "Ocultar opções avançadas"
"showLess": "Ocultar opções avançadas",
"goToSource": "Ir para o local de origem"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -371,7 +372,8 @@
"pickIconDescription": "Pesquise e selecione um ícone na biblioteca de ícones de materiais",
"shareLinksUpdatedSuccess": "Os links da barra lateral de compartilhamento foram atualizados com sucesso!",
"shareHome": "Compartilhar casa",
"linkIconPlaceholder": "Opcionalmente, especifique um nome de ícone de material"
"linkIconPlaceholder": "Opcionalmente, especifique um nome de ícone de material",
"dividerNamePlaceholder": "Texto do divisor (deixe em branco para mostrar uma linha)"
},
"success": {
"linkCopied": "Link copiado!"
@@ -627,7 +629,8 @@
"lastUpdate": "Última atualização{suffix}",
"latency": "Latência{suffix}",
"all": "Todos{suffix}",
"expiration": "Vencimento{suffix}"
"expiration": "Vencimento{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Regras de acesso",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Ir para o item",
"watchFile": "Ver ficheiro",
"showMore": "Mostrar opções avançadas",
"showLess": "Ocultar opções avançadas"
"showLess": "Ocultar opções avançadas",
"goToSource": "Ir para o local de origem"
},
"download": {
"downloadFile": "Descarregar ficheiro",
@@ -371,7 +372,8 @@
"pickIconDescription": "Procurar e selecionar um ícone na biblioteca de ícones de materiais",
"shareLinksUpdatedSuccess": "As ligações da barra lateral de partilha foram actualizadas com êxito!",
"shareHome": "Partilhar casa",
"linkIconPlaceholder": "Especificar opcionalmente um nome de ícone de material"
"linkIconPlaceholder": "Especificar opcionalmente um nome de ícone de material",
"dividerNamePlaceholder": "Texto do separador (deixar em branco para mostrar uma linha)"
},
"success": {
"linkCopied": "Link copiado!"
@@ -707,7 +709,8 @@
"lastUpdate": "Última atualização{suffix}",
"latency": "Latência{suffix}",
"all": "Todos{suffix}",
"expiration": "Prazo de validade{suffix}"
"expiration": "Prazo de validade{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Regras de acesso",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Du-te la articol",
"watchFile": "Fișier de vizionare",
"showMore": "Afișare opțiuni avansate",
"showLess": "Ascundeți opțiunile avansate"
"showLess": "Ascundeți opțiunile avansate",
"goToSource": "Du-te la locația sursei"
},
"download": {
"downloadFile": "Descarcă fișier",
@@ -371,7 +372,8 @@
"pickIconDescription": "Căutați și selectați o pictogramă din biblioteca Material Icons",
"shareLinksUpdatedSuccess": "Link-urile din bara laterală de partajare au fost actualizate cu succes!",
"shareHome": "Share Home",
"linkIconPlaceholder": "Opțional, specificați un nume de pictogramă de material"
"linkIconPlaceholder": "Opțional, specificați un nume de pictogramă de material",
"dividerNamePlaceholder": "Textul separatorului (lăsați gol pentru a afișa o linie)"
},
"success": {
"linkCopied": "Legătură copiată!"
@@ -627,7 +629,8 @@
"lastUpdate": "Ultima actualizare{suffix}",
"latency": "Latență{suffix}",
"all": "Toate{suffix}",
"expiration": "Expirare{suffix}"
"expiration": "Expirare{suffix}",
"divider": "Divizor{suffix}"
},
"access": {
"rules": "Reguli de acces",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Перейти к пункту",
"watchFile": "Смотреть файл",
"showMore": "Показать дополнительные параметры",
"showLess": "Скрыть дополнительные параметры"
"showLess": "Скрыть дополнительные параметры",
"goToSource": "Перейти к исходному местоположению"
},
"download": {
"downloadFile": "Скачать файл",
@@ -371,7 +372,8 @@
"pickIconDescription": "Найдите и выберите значок в библиотеке Material Icons.",
"shareLinksUpdatedSuccess": "Ссылки на боковой панели успешно обновлены!",
"shareHome": "Поделиться Домой",
"linkIconPlaceholder": "Опционально укажите имя значка материала"
"linkIconPlaceholder": "Опционально укажите имя значка материала",
"dividerNamePlaceholder": "Текст разделителя (оставьте пустым, чтобы показать строку)"
},
"success": {
"linkCopied": "Ссылка скопирована!"
@@ -627,7 +629,8 @@
"lastUpdate": "Последнее обновление{suffix}",
"latency": "Латентность{suffix}",
"all": "Все{suffix}",
"expiration": "Срок действия{suffix}"
"expiration": "Срок действия{suffix}",
"divider": "Разделитель{suffix}"
},
"access": {
"rules": "Правила доступа",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Prejsť na položku",
"watchFile": "Sledovať súbor",
"showMore": "Zobraziť rozšírené možnosti",
"showLess": "Skryť rozšírené možnosti"
"showLess": "Skryť rozšírené možnosti",
"goToSource": "Prejsť na zdrojové miesto"
},
"download": {
"downloadFile": "Stiahnuť súbor",
@@ -371,7 +372,8 @@
"pickIconDescription": "Vyhľadajte a vyberte ikonu z knižnice Material Icons",
"shareLinksUpdatedSuccess": "Zdieľanie bočného panela odkazy úspešne aktualizované!",
"shareHome": "Zdieľať Domov",
"linkIconPlaceholder": "Voliteľne zadajte názov ikony materiálu"
"linkIconPlaceholder": "Voliteľne zadajte názov ikony materiálu",
"dividerNamePlaceholder": "Text oddeľovača (nechajte prázdny, aby sa zobrazil riadok)"
},
"success": {
"linkCopied": "Odkaz skopírovaný!"
@@ -627,7 +629,8 @@
"lastUpdate": "Posledná aktualizácia{suffix}",
"latency": "Latencia{suffix}",
"all": "Všetky{suffix}",
"expiration": "Uplynutie platnosti{suffix}"
"expiration": "Uplynutie platnosti{suffix}",
"divider": "Rozdeľovač{suffix}"
},
"access": {
"rules": "Pravidlá prístupu",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Gå till artikel",
"watchFile": "Titta på filen",
"showMore": "Visa avancerade alternativ",
"showLess": "Dölj avancerade alternativ"
"showLess": "Dölj avancerade alternativ",
"goToSource": "Gå till källplatsen"
},
"download": {
"downloadFile": "Ladda ner fil",
@@ -371,7 +372,8 @@
"pickIconDescription": "Sök och välj en ikon från Material Icons-biblioteket",
"shareLinksUpdatedSuccess": "Dela sidofältets länkar uppdaterades framgångsrikt!",
"shareHome": "Dela hem",
"linkIconPlaceholder": "Ange eventuellt ett namn på materialikonen"
"linkIconPlaceholder": "Ange eventuellt ett namn på materialikonen",
"dividerNamePlaceholder": "Delningstext (lämna tom för att visa en rad)"
},
"success": {
"linkCopied": "Länk kopierad"
@@ -707,7 +709,8 @@
"lastUpdate": "Senaste uppdatering{suffix}",
"latency": "Fördröjning{suffix}",
"all": "Alla{suffix}",
"expiration": "Förfallodag{suffix}"
"expiration": "Förfallodag{suffix}",
"divider": "Uppdelare{suffix}"
},
"access": {
"rules": "Regler för åtkomst",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Öğeye git",
"watchFile": "Dosyayı izle",
"showMore": "Gelişmiş Seçenekleri Göster",
"showLess": "Gelişmiş Seçenekleri Gizle"
"showLess": "Gelişmiş Seçenekleri Gizle",
"goToSource": "Kaynak Konuma Git"
},
"download": {
"downloadFile": "Dosyayı indir",
@@ -371,7 +372,8 @@
"pickIconDescription": "Materyal Simgeleri kitaplığından bir simge arayın ve seçin",
"shareLinksUpdatedSuccess": "Kenar çubuğu bağlantılarını paylaşma başarıyla güncellendi!",
"shareHome": "Ev Paylaşımı",
"linkIconPlaceholder": "İsteğe bağlı olarak bir malzeme simgesi adı belirtin"
"linkIconPlaceholder": "İsteğe bağlı olarak bir malzeme simgesi adı belirtin",
"dividerNamePlaceholder": "Bölücü metin (bir çizgi göstermek için boş bırakın)"
},
"success": {
"linkCopied": "Link kopyalandı!"
@@ -627,7 +629,8 @@
"lastUpdate": "Son Güncelleme{suffix}",
"latency": "Gecikme{suffix}",
"all": "Tümü{suffix}",
"expiration": "Son kullanma tarihi{suffix}"
"expiration": "Son kullanma tarihi{suffix}",
"divider": "Bölücü{suffix}"
},
"access": {
"rules": "Erişim kuralları",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "Перейти до пункту",
"watchFile": "Переглянути файл",
"showMore": "Показати додаткові параметри",
"showLess": "Приховати додаткові параметри"
"showLess": "Приховати додаткові параметри",
"goToSource": "Перейдіть до Розташування джерела"
},
"download": {
"downloadFile": "Завантажити файл",
@@ -371,7 +372,8 @@
"pickIconDescription": "Знайдіть і виберіть піктограму з бібліотеки піктограм матеріалів",
"shareLinksUpdatedSuccess": "Посилання на бічну панель обміну успішно оновлено!",
"shareHome": "Поділитися Додому",
"linkIconPlaceholder": "За бажанням вкажіть назву іконки матеріалу"
"linkIconPlaceholder": "За бажанням вкажіть назву іконки матеріалу",
"dividerNamePlaceholder": "Текст роздільника (залиште порожнім, щоб показати лінію)"
},
"success": {
"linkCopied": "Посилання скопійоване!"
@@ -707,7 +709,8 @@
"lastUpdate": "Останнє оновлення{suffix}",
"latency": "Затримка{suffix}",
"all": "Усе.{suffix}",
"expiration": "Закінчення терміну дії{suffix}"
"expiration": "Закінчення терміну дії{suffix}",
"divider": "Перегородка{suffix}"
},
"access": {
"rules": "Правила доступу",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "转到项目",
"watchFile": "观看文件",
"showMore": "显示高级选项",
"showLess": "隐藏高级选项"
"showLess": "隐藏高级选项",
"goToSource": "转到源位置"
},
"download": {
"downloadFile": "下载文件",
@@ -371,7 +372,8 @@
"pickIconDescription": "从材质图标库中搜索并选择图标",
"shareLinksUpdatedSuccess": "共享侧边栏链接更新成功!",
"shareHome": "共享主页",
"linkIconPlaceholder": "可选择指定材料图标名称"
"linkIconPlaceholder": "可选择指定材料图标名称",
"dividerNamePlaceholder": "分隔线文本(留空以显示一行)"
},
"success": {
"linkCopied": "链接已复制!"
@@ -707,7 +709,8 @@
"lastUpdate": "最新更新{suffix}",
"latency": "延迟{suffix}",
"all": "全部{suffix}",
"expiration": "到期{suffix}"
"expiration": "到期{suffix}",
"divider": "分割器{suffix}"
},
"access": {
"rules": "访问规则",
+6 -3
View File
@@ -31,7 +31,8 @@
"goToItem": "前往項目",
"watchFile": "觀看檔案",
"showMore": "顯示進階選項",
"showLess": "隱藏進階選項"
"showLess": "隱藏進階選項",
"goToSource": "前往來源位置"
},
"download": {
"downloadFile": "下載檔案",
@@ -371,7 +372,8 @@
"pickIconDescription": "從 Material Icons 圖示庫搜尋並選取圖示",
"shareLinksUpdatedSuccess": "分享側邊欄連結更新成功!",
"shareHome": "分享首頁",
"linkIconPlaceholder": "可選擇指定材質圖示名稱"
"linkIconPlaceholder": "可選擇指定材質圖示名稱",
"dividerNamePlaceholder": "分割線文字(留空顯示一行)"
},
"success": {
"linkCopied": "連結已複製!"
@@ -707,7 +709,8 @@
"lastUpdate": "最後更新{suffix}",
"latency": "延遲{suffix}",
"all": "全部{suffix}",
"expiration": "到期日{suffix}"
"expiration": "到期日{suffix}",
"divider": "分割器{suffix}"
},
"access": {
"rules": "存取規則",
@@ -34,12 +34,12 @@ test("check previously created share has correct sidebar links", async ({ page,
// navigate to the share sidebar
await page.goto("/public/share/" + shareHash);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - text-files");
// sidebar should have three items (ShareInfo, Download, Edit Share)
// sidebar should have 3 items (ShareInfo, Download, Go to source location)
await expect(page.locator('.sidebar-links .inner-card').locator('a')).toHaveCount(3);
// check items exist
await page.locator('a[aria-label="Share QR Code and Info"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="Download"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="Edit Share"]').waitFor({ state: 'visible' });
await page.locator('i[aria-label="Edit Share"]').waitFor({ state: 'visible' });
checkForErrors();
});
@@ -76,12 +76,11 @@ test("edit previously created links and ensure they are updated", async ({ page,
// navigate to the share sidebar
await page.goto("/public/share/" + shareHash);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - text-files");
// sidebar should have four items (ShareInfo, Download, New Custom Link, Edit Share)
// sidebar should have 4 items (ShareInfo, Download, New Custom Link, Go to source location)
await expect(page.locator('.sidebar-links .inner-card').locator('a')).toHaveCount(4);
// check items exist
await page.locator('a[aria-label="Share QR Code and Info"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="Download"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="New Custom Link"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="Edit Share"]').waitFor({ state: 'visible' });
checkForErrors();
});