Compare commits

...
18 Commits
Author SHA1 Message Date
Graham Steffaniak e0a70b9257 updated deps (#2323) 2026-04-20 16:39:05 -05:00
Graham Steffaniak 1cd9050414 updated line count api (#2320) 2026-04-19 14:28:07 -05:00
Graham Steffaniak e2b6f06ee8 fixed single file share (#2319) 2026-04-19 14:13:57 -05:00
Graham Steffaniak ab862f8b74 small bug fixes (#2318) 2026-04-19 13:36:21 -05:00
Graham Steffaniak 5ac617d7b6 Small source size change (#2315) 2026-04-18 00:07:57 -05:00
Graham Steffaniak 838a976868 updated deps (#2313) 2026-04-18 00:04:27 -05:00
Graham Steffaniak 4c3eb73923 Update password change requirements (#2312) 2026-04-17 16:10:34 -05:00
Graham Steffaniak 94852d4fd3 updated with a couple more fixes (#2311) 2026-04-17 15:32:20 -05:00
Kevin B. b00be6717b fix search shortcut and pdf previews (#2307) 2026-04-17 12:08:54 -05:00
Graham Steffaniak 693aaa65ce make tests more reliable 2026-04-16 19:31:09 -05:00
Graham Steffaniak e3b5f08dfe apply all defaults, some were missing (#2306) 2026-04-16 19:25:56 -05:00
Graham Steffaniak b5062ce2f6 fix legacy token (#2305) 2026-04-16 19:25:45 -05:00
Graham Steffaniak 7bf4078371 several bugfixes related to access, otp, password (#2304) 2026-04-16 13:28:03 -05:00
Nebula bb8336dd3f Add .devcontainer and fix 'make dev' startup (#2286) 2026-04-15 14:11:45 -05:00
Stephan Paternotte 5f5e4d7838 Update nl.json (#2293) 2026-04-14 19:23:54 -05:00
Graham Steffaniak 24ca3094ed updated with pwa fixes 2026-04-14 19:21:26 -05:00
Kevin B. 9232a77eaa fix replace-rename prompt that wasn't working in some cases (#2285) 2026-04-14 11:11:13 -05:00
Kevin B. e26ecc10ff Fix copy to clipboard and simplify code (#2281) 2026-04-13 16:43:46 -05:00
105 changed files with 1774 additions and 1186 deletions

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
{
"name": "filebrowser",
"image": "mcr.microsoft.com/devcontainers/go:1.25-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"containerEnv": {
"CGO_ENABLED": "1",
"FILEBROWSER_DEVMODE": "true"
},
"forwardPorts": [80],
"portsAttributes": {
"80": {
"label": "filebrowser app",
"onAutoForward": "notify"
}
},
"postCreateCommand": "make setup",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker"
]
}
}
}
+2
View File
@@ -1,4 +1,6 @@
*.db
*.db.old
*.sqlite
*.bak
*.log
*.mjs
+27
View File
@@ -2,11 +2,38 @@
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.3.6
**BugFixes**:
- file watcher not working for files and not respecting line count (#2314)
- "source" type link shows up multiple times when adding sidebar links if user has multiple sources.
- JWT ES256 results in error (#2188)
- single file shares not loading editor text content (#2122)
## v1.3.5
**Notes**:
- any password change or admin user property change for a password user requires reauthentication (#2112)
**BugFixes**:
- PWA icon fixes (#2292)
- deny-rule'd folders visible in directory listings (regression from v1.2.4-stable) (#2295)
- OTP and password requirement fixes (#2112) (#2263)
- CLI not picking up config properly
- Storage usage numbers on first load adjusted.
- Some items in userDefaults are not setting defaults for new users properly #2278
- Fix copy to clipboard and simplify code (#2281) (#2274)
- Fix search shortcut `/` and pdf previews (#2307)
- Special permissions bit not set when declared in createDirectoryPermission (#2283)
- Execute permission altered after editing sh script with FB Quantum (#2309)
- Source is red but working (#2289)
## v1.3.4
**New Features**:
- Epub placementg url anchors to bookmark a specific location on the doc.
- Add Requirement for Current Password When Changing Account Password (#2112)
- Copy file path to clipborad through right click (#2204)
**Notes**:
- user scope editing has path picker and filesystem validation
+3 -2
View File
@@ -18,5 +18,6 @@ COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser set -u testuser1,testuser1
RUN ./filebrowser set rule -f /tests/playwright-files -p / -r user -v admin -allow -c config.yaml
RUN ./filebrowser set rule -f /tests/playwright-files -p /excluded -r user -v admin -c config.yaml
RUN ./filebrowser set rule -s access -p / -r user -v admin -allow -c config.yaml
RUN ./filebrowser set rule -s access -p /excluded -r user -v admin -c config.yaml
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+107
View File
@@ -1,6 +1,7 @@
package files
import (
"errors"
"fmt"
"os"
"path/filepath"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
liberrors "github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
dbsql "github.com/gtsteffaniak/filebrowser/backend/database/sql"
"github.com/gtsteffaniak/filebrowser/backend/indexing"
@@ -88,6 +90,111 @@ func Test_GetRealPath(t *testing.T) {
}
}
// Regression: PUT / save must not os.Chmod to PermFile for existing files, or +x (and other mode bits) are lost (#2309).
func TestWriteFilePreservesExecutableBit(t *testing.T) {
cacheDir := t.TempDir()
originalCache := settings.Config.Server.CacheDir
settings.Config.Server.CacheDir = cacheDir
t.Cleanup(func() { settings.Config.Server.CacheDir = originalCache })
fileutils.SetFsPermissions(0o644, 0o755)
if indexing.GetIndexDB() == nil {
db, _, err := dbsql.NewIndexDB("test_writefile_exec", "OFF", 1000, 32, false)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
indexing.SetIndexDBForTesting(db)
}
root := t.TempDir()
scriptName := "backupscript.sh"
scriptAbs := filepath.Join(root, scriptName)
if err := os.WriteFile(scriptAbs, []byte("#!/bin/sh\necho hi\n"), 0o755); err != nil {
t.Fatal(err)
}
sourceName := "test_writefile_exec"
indexing.Initialize(&settings.Source{
Name: sourceName,
Path: root,
}, false, false)
idx := indexing.GetIndex(sourceName)
if idx == nil {
t.Fatal("Failed to get test index")
}
stopPoll := startScannerStatusPoll(t, idx, 1)
defer stopPoll()
waitForScannerReady(t, idx)
indexPath := "/" + scriptName
if err := WriteFile(sourceName, indexPath, strings.NewReader("#!/bin/sh\necho bye\n")); err != nil {
t.Fatalf("WriteFile: %v", err)
}
st, err := os.Stat(scriptAbs)
if err != nil {
t.Fatal(err)
}
if st.Mode()&0o111 == 0 {
t.Fatalf("executable bits cleared after save: mode=%v", st.Mode())
}
indexing.StopAllScanners()
time.Sleep(50 * time.Millisecond)
indexing.ClearTestIndices()
}
func TestWriteFileRejectsExistingDirectory(t *testing.T) {
cacheDir := t.TempDir()
originalCache := settings.Config.Server.CacheDir
settings.Config.Server.CacheDir = cacheDir
t.Cleanup(func() { settings.Config.Server.CacheDir = originalCache })
fileutils.SetFsPermissions(0o644, 0o755)
if indexing.GetIndexDB() == nil {
db, _, err := dbsql.NewIndexDB("test_writefile_isdir", "OFF", 1000, 32, false)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
indexing.SetIndexDBForTesting(db)
}
root := t.TempDir()
dirName := "folder"
if err := os.Mkdir(filepath.Join(root, dirName), 0o755); err != nil {
t.Fatal(err)
}
sourceName := "test_writefile_isdir"
indexing.Initialize(&settings.Source{
Name: sourceName,
Path: root,
}, false, false)
idx := indexing.GetIndex(sourceName)
if idx == nil {
t.Fatal("Failed to get test index")
}
stopPoll := startScannerStatusPoll(t, idx, 1)
defer stopPoll()
waitForScannerReady(t, idx)
err := WriteFile(sourceName, "/"+dirName, strings.NewReader("file content"))
if err == nil {
t.Fatal("expected error when path is an existing directory")
}
if !errors.Is(err, liberrors.ErrIsDirectory) {
t.Fatalf("expected ErrIsDirectory, got: %v", err)
}
indexing.StopAllScanners()
time.Sleep(50 * time.Millisecond)
indexing.ClearTestIndices()
}
func TestSortItems(t *testing.T) {
tests := []struct {
name string
+21 -23
View File
@@ -226,18 +226,17 @@ func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *us
return response, err // Path excluded by index rules OR doesn't exist
}
// Build response
response.FileInfo = *info
response.RealPath = filepath.Join(idx.Path, indexPath)
response.Source = opts.Source
// Layer 3: FILTER CHILDREN (user access)
// Remove child items THIS user can't access
// otherwise response keeps unfiltered Folders/Files while CheckChildItemAccess only mutates info.
if info.Type == "directory" {
if err := access.CheckChildItemAccess(info, idx, user.Username); err != nil {
return response, err
}
}
// Build response
response.FileInfo = *info
response.RealPath = filepath.Join(idx.Path, indexPath)
response.Source = opts.Source
if share != nil && user.Permissions.Share && opts.ShowSharedAttr {
for i := range response.Files {
file := &response.Files[i]
@@ -762,22 +761,21 @@ func WriteFile(source, path string, in io.Reader) error {
if err != nil {
return err
}
applyDefaultFilePerm := false
var stat os.FileInfo
// Check if the destination exists and is a directory
if stat, err = os.Stat(realPath); err == nil {
if stat.IsDir() {
// If it's a directory and we're trying to create a file, remove the directory first
err = os.RemoveAll(realPath)
if err != nil {
return fmt.Errorf("could not remove existing directory to create file: %v", err)
}
stat, err = os.Stat(realPath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("stat file: %w", err)
}
// If file exists, its permissions will be preserved (O_TRUNC doesn't change permissions)
applyDefaultFilePerm = true
} else if stat.IsDir() {
return fmt.Errorf("%w: %q", errors.ErrIsDirectory, path)
}
// Open the file for writing (create if it doesn't exist, truncate if it does)
// For new files: permissions are set to fileutils.PermFile (subject to umask, which is usually acceptable)
// For existing files: permissions are preserved automatically (O_TRUNC doesn't change them)
// For new files: permissions are set to fileutils.PermFile (subject to umask, then Chmod bypasses umask)
// For existing regular files: do not Chmod so saves preserve mode and special bits
file, err := os.OpenFile(realPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileutils.PermFile)
if err != nil {
return err
@@ -790,11 +788,11 @@ func WriteFile(source, path string, in io.Reader) error {
return err
}
// Explicitly set file permissions to bypass umask
err = os.Chmod(realPath, fileutils.PermFile)
if err != nil {
// Handle chmod error gracefully
logger.Debugf("Could not set file permissions for %s (this may be expected in restricted environments): %v", realPath, err)
if applyDefaultFilePerm {
err = os.Chmod(realPath, fileutils.PermFile)
if err != nil {
logger.Debugf("Could not set file permissions for %s (this may be expected in restricted environments): %v", realPath, err)
}
}
// Refresh the file itself
+21 -3
View File
@@ -12,9 +12,27 @@ import (
var PermFile os.FileMode
var PermDir os.FileMode
func SetFsPermissions(PermFileOctal os.FileMode, PermDirOctal os.FileMode) {
PermFile = PermFileOctal
PermDir = PermDirOctal
// SetFsPermissions sets create modes from Unix chmod(2)-style values (for example
// values produced by strconv.ParseUint(s, 8, 32)). setuid, setgid, and sticky
// bits must be converted for Go's os.FileMode; a plain cast from Unix octal is
// incorrect for those bits.
func SetFsPermissions(unixFileMode, unixDirMode uint32) {
PermFile = unixModeToFileMode(unixFileMode)
PermDir = unixModeToFileMode(unixDirMode)
}
func unixModeToFileMode(u uint32) os.FileMode {
m := os.FileMode(u & 0777)
if u&0o4000 != 0 {
m |= os.ModeSetuid
}
if u&0o2000 != 0 {
m |= os.ModeSetgid
}
if u&0o1000 != 0 {
m |= os.ModeSticky
}
return m
}
// MoveFile moves a file from src to dst.
+17 -1
View File
@@ -1,6 +1,22 @@
package fileutils
import "testing"
import (
"os"
"testing"
)
func TestUnixModeToFileMode_setgid(t *testing.T) {
m := unixModeToFileMode(0o2770)
if got := m.Perm(); got != 0o770 {
t.Fatalf("Perm() = %#o, want 0770", got)
}
if m&os.ModeSetgid == 0 {
t.Fatal("expected ModeSetgid")
}
if m&os.ModeSetuid != 0 || m&os.ModeSticky != 0 {
t.Fatalf("unexpected special bits: %#o", m)
}
}
func TestCommonPrefix(t *testing.T) {
testCases := map[string]struct {
+3 -3
View File
@@ -15,7 +15,8 @@ import (
// JSONAuth is a json implementation of an Auther.
type JSONAuth struct {
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
DisableOtp bool `json:"disableOtp" yaml:"disableOtp"`
}
// Auth authenticates the user via a json in content body.
@@ -58,8 +59,7 @@ func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.U
if getErr != nil {
return nil, fmt.Errorf("unable to get user from store: %v", err)
}
// check for OTP for password
if user.TOTPSecret != "" {
if user.TOTPSecret != "" && !auther.DisableOtp {
if totpCode == "" {
return nil, errors.ErrNoTotpProvided
}
+102 -7
View File
@@ -1,7 +1,12 @@
package auth
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
@@ -47,14 +52,12 @@ func VerifyExternalJWT(tokenString string, secret string, algorithm string, user
if expectedMethod == nil {
return nil, fmt.Errorf("unsupported signing algorithm: %s", algorithm)
}
if token.Method.Alg() != expectedMethod.Alg() {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Return the secret key for HMAC algorithms
// For RSA/ECDSA, this would need to be a public key
return []byte(secret), nil
return jwtVerificationKey(secret, token.Method, algorithm)
})
if err != nil {
@@ -100,10 +103,102 @@ func VerifyExternalJWT(tokenString string, secret string, algorithm string, user
return username, claimsMap, nil
}
func jwtVerificationKey(secret string, method jwt.SigningMethod, algorithm string) (interface{}, error) {
switch method.(type) {
case *jwt.SigningMethodHMAC:
return []byte(secret), nil
case *jwt.SigningMethodRSA:
key, err := parsePublicKeyFromPEM(secret)
if err != nil {
return nil, err
}
rsaKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("JWT secret must contain an RSA public key in PEM format for algorithm %s", algorithm)
}
return rsaKey, nil
case *jwt.SigningMethodECDSA:
key, err := parsePublicKeyFromPEM(secret)
if err != nil {
return nil, err
}
ecKey, ok := key.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("JWT secret must contain an ECDSA public key in PEM format for algorithm %s", algorithm)
}
if err := ecdsaCurveMatchesAlgorithm(algorithm, ecKey); err != nil {
return nil, err
}
return ecKey, nil
default:
return nil, fmt.Errorf("unsupported JWT signing method: %s", method.Alg())
}
}
func ecdsaCurveMatchesAlgorithm(algorithm string, pub *ecdsa.PublicKey) error {
var want elliptic.Curve
switch algorithm {
case "ES256":
want = elliptic.P256()
case "ES384":
want = elliptic.P384()
case "ES512":
want = elliptic.P521()
default:
return nil
}
if pub.Curve != want {
return fmt.Errorf("ECDSA public key curve does not match algorithm %s", algorithm)
}
return nil
}
func parsePublicKeyFromPEM(pemData string) (interface{}, error) {
rest := []byte(pemData)
var lastErr error
for len(rest) > 0 {
block, rem := pem.Decode(rest)
if block == nil {
break
}
rest = rem
var key interface{}
var err error
switch block.Type {
case "CERTIFICATE":
var cert *x509.Certificate
cert, err = x509.ParseCertificate(block.Bytes)
if err == nil {
key = cert.PublicKey
}
case "PUBLIC KEY":
key, err = x509.ParsePKIXPublicKey(block.Bytes)
case "RSA PUBLIC KEY":
key, err = x509.ParsePKCS1PublicKey(block.Bytes)
case "EC PUBLIC KEY":
key, err = x509.ParsePKIXPublicKey(block.Bytes)
default:
continue
}
if err != nil {
lastErr = err
continue
}
if key != nil {
return key, nil
}
}
if lastErr != nil {
return nil, fmt.Errorf("failed to parse JWT public key from PEM: %w", lastErr)
}
return nil, fmt.Errorf("no public key found in JWT secret (expected PEM: CERTIFICATE, PUBLIC KEY, RSA PUBLIC KEY, or EC PUBLIC KEY)")
}
// ExtractGroupsFromClaims extracts groups from JWT claims based on the configured groups claim field
func ExtractGroupsFromClaims(claims map[string]interface{}, groupsClaimField string) []string {
var groups []string
if groupsVal, ok := claims[groupsClaimField]; ok {
switch v := groupsVal.(type) {
case []interface{}:
@@ -129,6 +224,6 @@ func ExtractGroupsFromClaims(claims map[string]interface{}, groupsClaimField str
}
}
}
return groups
}
+142
View File
@@ -1,6 +1,14 @@
package auth_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"testing"
"time"
@@ -144,6 +152,140 @@ func TestVerifyExternalJWT_CustomUserIdentifier(t *testing.T) {
}
}
func TestVerifyExternalJWT_ES256_PublicKeyPEM(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
pubDER, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
t.Fatalf("MarshalPKIXPublicKey: %v", err)
}
pubPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}))
username := "ec-user"
claims := jwt.MapClaims{
"sub": username,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
tokenString, err := token.SignedString(priv)
if err != nil {
t.Fatalf("SignedString: %v", err)
}
got, _, err := auth.VerifyExternalJWT(tokenString, pubPEM, "ES256", "sub")
if err != nil {
t.Fatalf("VerifyExternalJWT: %v", err)
}
if got != username {
t.Errorf("username: got %q want %q", got, username)
}
}
func TestVerifyExternalJWT_RS256_PublicKeyPEM(t *testing.T) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
pubDER, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
t.Fatalf("MarshalPKIXPublicKey: %v", err)
}
pubPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}))
username := "rsa-user"
claims := jwt.MapClaims{
"sub": username,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
tokenString, err := token.SignedString(priv)
if err != nil {
t.Fatalf("SignedString: %v", err)
}
got, _, err := auth.VerifyExternalJWT(tokenString, pubPEM, "RS256", "sub")
if err != nil {
t.Fatalf("VerifyExternalJWT: %v", err)
}
if got != username {
t.Errorf("username: got %q want %q", got, username)
}
}
func TestVerifyExternalJWT_ES256_WrongCurveRejected(t *testing.T) {
p256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
p384Priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
verifyDER, err := x509.MarshalPKIXPublicKey(&p384Priv.PublicKey)
if err != nil {
t.Fatalf("MarshalPKIXPublicKey: %v", err)
}
verifyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: verifyDER}))
claims := jwt.MapClaims{
"sub": "u",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
tokenString, err := token.SignedString(p256Priv)
if err != nil {
t.Fatalf("SignedString: %v", err)
}
_, _, err = auth.VerifyExternalJWT(tokenString, verifyPEM, "ES256", "sub")
if err == nil {
t.Fatal("expected error when PEM public key curve does not match algorithm")
}
}
func TestVerifyExternalJWT_CertificatePEM(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
tpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "jwt-test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
}
certDER, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("CreateCertificate: %v", err)
}
certPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
username := "cert-user"
claims := jwt.MapClaims{
"sub": username,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
tokenString, err := token.SignedString(priv)
if err != nil {
t.Fatalf("SignedString: %v", err)
}
got, _, err := auth.VerifyExternalJWT(tokenString, certPEM, "ES256", "sub")
if err != nil {
t.Fatalf("VerifyExternalJWT: %v", err)
}
if got != username {
t.Errorf("username: got %q want %q", got, username)
}
}
func TestExtractGroupsFromClaims_ArrayOfStrings(t *testing.T) {
claims := map[string]interface{}{
"groups": []string{"admin", "users"},
+70 -60
View File
@@ -23,7 +23,7 @@ var (
)
// return bool to indicate if the program should continue running
func runCLI() bool {
func runCLI() (bool, bool) {
generateYaml()
@@ -53,85 +53,100 @@ func runCLI() bool {
// Show help if requested
if help {
generalUsage()
return false
return false, false
}
// Create a new FlagSet for the 'set' subcommand
setCmd := flag.NewFlagSet("set", flag.ExitOnError)
var user, dbConfig string
var asAdmin bool
var fsPath, indexPath, ruleCategory, value string
var sourceName, indexPath, ruleCategory, value string
var allow bool
setCmd.StringVar(&user, "u", "", "Comma-separated username and password: \"set -u <username>,<password>\"")
setCmd.BoolVar(&asAdmin, "a", false, "Create a user as admin user, used in combination with -u")
setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml")
setCmd.StringVar(&fsPath, "f", "", "Real filesystem path (e.g. /mnt/storage) for rule command")
setCmd.StringVar(&sourceName, "s", "", "source name to apply the rule to")
setCmd.StringVar(&indexPath, "p", "", "Index path (e.g. /secret) for rule command")
setCmd.StringVar(&ruleCategory, "r", "", "Rule category: 'user', 'group', or 'all' (for deny only)")
setCmd.StringVar(&value, "v", "", "Value: username or groupname (not required if ruleCategory is 'all')")
setCmd.BoolVar(&allow, "allow", false, "Allow access (default: false, which means deny)")
// Parse subcommand flags only if a subcommand is specified
storeCfg := configPath
cliSubcmd := ""
var setKind string
if len(os.Args) > 1 {
switch os.Args[1] {
case "setup":
createConfig(configPath)
return false
case "set":
if len(os.Args) < 3 {
fmt.Printf("error: missing subcommand for 'set'. Use 'set user' or 'set rule'\n")
os.Exit(1)
}
subcommand := os.Args[2]
// Handle old syntax: "set -u user,pass -c config.yaml"
if subcommand == "-u" {
// Parse flags starting from os.Args[2:] to include the -u flag
err := setCmd.Parse(os.Args[2:])
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
logger.Debugf("setUser called with args: %v, config: %v, admin: %v", os.Args, dbConfig, asAdmin)
err = setUser(dbConfig, asAdmin)
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
return false
}
// New pattern: subcommand-based (e.g., "set user" or "set rule")
// Parse flags starting from os.Args[3:] to skip the subcommand
err := setCmd.Parse(os.Args[3:])
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
switch subcommand {
case "rule":
err := setRule(dbConfig, fsPath, indexPath, ruleCategory, value, allow)
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
return false
default:
fmt.Printf("error: unknown subcommand '%s' for 'set'. Use 'set user' or 'set rule'\n", subcommand)
os.Exit(1)
}
case "version":
fmt.Printf(`FileBrowser Quantum - A modern web-based file manager
Version : %v
Commit : %v
Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
`, version.Version, version.CommitSHA, version.Version)
return false
`, version.Version, version.CommitSHA, version.Version)
return false, false
case "setup":
cliSubcmd = "setup"
case "set":
cliSubcmd = "set"
if len(os.Args) < 3 {
fmt.Printf("error: missing subcommand for 'set'. Use 'set user' or 'set rule'\n")
os.Exit(1)
}
setKind = os.Args[2]
var err error
// Handle old syntax: "set -u user,pass -c config.yaml"
if setKind == "-u" {
err = setCmd.Parse(os.Args[2:])
} else {
// New pattern: subcommand-based (e.g., "set user" or "set rule")
err = setCmd.Parse(os.Args[3:])
}
if err != nil {
setCmd.PrintDefaults()
os.Exit(1)
}
storeCfg = dbConfig
}
}
return true
dbExists := getStore(storeCfg)
switch cliSubcmd {
case "setup":
createConfig(configPath)
return false, dbExists
case "set":
if setKind == "-u" {
logger.Debugf("setUser called with args: %v, config: %v, admin: %v", os.Args, dbConfig, asAdmin)
err := setUser(dbConfig, asAdmin)
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
return false, dbExists
}
switch setKind {
case "rule":
sourceInfo, ok := settings.Config.Server.NameToSource[sourceName] // backend source is path
if !ok {
fmt.Printf("error: invalid source name: %s\n", sourceName)
os.Exit(1)
}
err := setRule(dbConfig, sourceInfo.Path, indexPath, ruleCategory, value, allow)
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
return false, dbExists
default:
fmt.Printf("error: unknown subcommand '%s' for 'set'. Use 'set user' or 'set rule'\n", setKind)
os.Exit(1)
}
return false, dbExists
default:
return true, dbExists
}
}
func setRule(dbConfig, fsPath, indexPath, ruleCategory, value string, allow bool) error {
@@ -148,10 +163,6 @@ func setRule(dbConfig, fsPath, indexPath, ruleCategory, value string, allow bool
if ruleCategory != "all" && value == "" {
return fmt.Errorf("value is required when ruleCategory is 'user' or 'group': use -v <username|groupname>")
}
// Initialize store and settings
_ = getStore(dbConfig) // ignore bool check
// Apply the rule based on allow flag and ruleCategory
var err error
if allow {
@@ -202,7 +213,6 @@ func setUser(dbConfig string, asAdmin bool) error {
}
username := userInfo[0]
password := userInfo[1]
_ = getStore(dbConfig) // ignore bool check
user, err := store.Users.Get(username)
if err != nil {
newUser := users.User{
+5 -6
View File
@@ -54,10 +54,14 @@ commands:
}
func StartFilebrowser() {
keepGoing := runCLI()
keepGoing, dbExists := runCLI()
if !keepGoing {
return
}
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
if !dbExists {
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
}
if !settings.Config.Server.DisableUpdateCheck {
info, _ := utils.CheckForUpdates()
if info.LatestVersion != "" {
@@ -76,11 +80,6 @@ func StartFilebrowser() {
done := make(chan struct{}) // Signals server has stopped
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
dbExists := getStore(configPath)
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
if !dbExists {
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
}
// Dev mode enables development features like template hot-reloading
_, err := os.Stat("http/dist")
+1 -1
View File
@@ -19,7 +19,7 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrNoTotpProvided = errors.New("OTP code is required for user")
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not yet configured")
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not configured")
ErrUnauthorized = errors.New("user unauthorized")
ErrNotIndexed = errors.New("directory or item excluded from indexing")
ErrNotViewable = errors.New("directory or item is not viewable")
@@ -0,0 +1,192 @@
package settings
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
)
// TestApplyUserDefaults_copiesUserDefaultsOntoUser guards against drift between
// [UserDefaults] and [ApplyUserDefaults] (regression for #2278-style bugs).
func TestApplyUserDefaults_copiesUserDefaultsOntoUser(t *testing.T) {
saved := Config
defer func() { Config = saved }()
dark := false
dlFalse := false
Config = Settings{
Server: Server{},
UserDefaults: UserDefaults{
EditorQuickSave: true,
HideSidebarFileActions: true,
DisableQuickToggles: true,
DisableSearchOptions: true,
StickySidebar: false,
HideFilesInTree: true,
DarkMode: &dark,
Locale: "de",
ViewMode: "list",
SingleClick: true,
ShowHidden: true,
DateFormat: true,
GallerySize: 7,
ThemeColor: "var(--red)",
QuickDownload: true,
DisablePreviewExt: ".exe",
DisableViewingExt: ".bat",
DisableSettings: true,
LockPassword: true,
DisableUpdateNotifications: true,
DeleteWithoutConfirming: true,
DeleteAfterArchive: false,
DisableOfficePreviewExt: "legacy",
DisableOnlyOfficeExt: ".x",
CustomTheme: "Night",
ShowSelectMultiple: true,
DebugOffice: true,
PreferEditorForMarkdown: true,
LoginMethod: "password",
FileLoading: users.FileLoading{
MaxConcurrent: 3,
UploadChunkSize: 5,
DownloadChunkSize: 2,
},
Permissions: UserDefaultsPermissions{
Api: true,
Admin: true,
Modify: true,
Share: true,
Realtime: true,
Delete: true,
Create: true,
Download: &dlFalse,
},
Preview: UserDefaultsPreview{
DisableHideSidebar: true,
Image: boolPtr(false),
Video: boolPtr(false),
Audio: boolPtr(false),
MotionVideoPreview: boolPtr(false),
Office: boolPtr(false),
PopUp: boolPtr(false),
AutoplayMedia: boolPtr(false),
DefaultMediaPlayer: true,
Folder: boolPtr(false),
Models: boolPtr(false),
},
},
}
u := &users.User{Username: "alice"}
ApplyUserDefaults(u)
want := users.User{
DisableSettings: true,
LockPassword: true,
LoginMethod: users.LoginMethodPassword,
Permissions: users.Permissions{
Api: true,
Admin: true,
Modify: true,
Share: true,
Realtime: true,
Delete: true,
Create: true,
Download: false,
},
NonAdminEditable: users.NonAdminEditable{
EditorQuickSave: true,
HideSidebarFileActions: true,
DisableQuickToggles: true,
DisableSearchOptions: true,
StickySidebar: false,
HideFilesInTree: true,
DarkMode: false,
Locale: "de",
ViewMode: "list",
SingleClick: true,
ShowHidden: true,
DateFormat: true,
GallerySize: 7,
ThemeColor: "var(--red)",
QuickDownload: true,
DisablePreviewExt: ".exe",
DisableViewingExt: ".bat",
DisableUpdateNotifications: true,
DisableOfficePreviewExt: "legacy",
DisableOnlyOfficeExt: ".x",
CustomTheme: "Night",
ShowSelectMultiple: true,
DebugOffice: true,
DeleteWithoutConfirming: true,
DeleteAfterArchive: false,
PreferEditorForMarkdown: true,
FileLoading: users.FileLoading{
MaxConcurrent: 3,
UploadChunkSize: 5,
DownloadChunkSize: 2,
},
Preview: users.Preview{
DisableHideSidebar: true,
Image: false,
Video: false,
Audio: false,
MotionVideoPreview: false,
Office: false,
PopUp: false,
AutoplayMedia: false,
DefaultMediaPlayer: true,
Folder: false,
Models: false,
},
},
}
if diff := cmp.Diff(want.Permissions, u.Permissions); diff != "" {
t.Fatalf("Permissions mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(want.NonAdminEditable, u.NonAdminEditable); diff != "" {
t.Fatalf("NonAdminEditable mismatch (-want +got):\n%s", diff)
}
if u.DisableSettings != want.DisableSettings {
t.Fatalf("DisableSettings: got %v want %v", u.DisableSettings, want.DisableSettings)
}
if u.LockPassword != want.LockPassword {
t.Fatalf("LockPassword: got %v want %v", u.LockPassword, want.LockPassword)
}
if u.LoginMethod != want.LoginMethod {
t.Fatalf("LoginMethod: got %q want %q", u.LoginMethod, want.LoginMethod)
}
}
func TestApplyUserDefaults_setsLoginMethodWhenEmpty(t *testing.T) {
saved := Config
defer func() { Config = saved }()
Config = Settings{
Server: Server{},
UserDefaults: UserDefaults{LoginMethod: "jwt"},
}
u := &users.User{Username: "x"}
ApplyUserDefaults(u)
if u.LoginMethod != users.LoginMethodJwt {
t.Fatalf("LoginMethod: got %q want jwt", u.LoginMethod)
}
}
func TestApplyUserDefaults_preservesLoginMethodWhenAlreadySet(t *testing.T) {
saved := Config
defer func() { Config = saved }()
Config = Settings{
Server: Server{},
UserDefaults: UserDefaults{LoginMethod: "password"},
}
u := &users.User{Username: "x", LoginMethod: users.LoginMethodProxy}
ApplyUserDefaults(u)
if u.LoginMethod != users.LoginMethodProxy {
t.Fatalf("LoginMethod: got %q want proxy", u.LoginMethod)
}
}
+2 -2
View File
@@ -106,12 +106,12 @@ type LdapConfig struct {
}
// JwtAuthConfig configures external JWT token authentication
// Similar to Grafana's JWT auth: accepts external JWT tokens signed with a shared secret
// Similar to Grafana's JWT auth: verifies HS* tokens with a shared secret, or RS*/ES* with a PEM public key (or certificate)
// The query parameter is hardcoded to "jwt" (e.g. ?jwt=<token>)
type JwtAuthConfig struct {
AuthCommon `json:",inline"`
Header string `json:"header"` // HTTP header to look for JWT token (e.g. X-JWT-Assertion). Default is "X-JWT-Assertion"
Secret string `json:"secret"` // secret: shared secret key for verifying JWT token signatures (required)
Secret string `json:"secret"` // Shared secret key/bytes for verifying JWT token signatures (required, eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)
Algorithm string `json:"algorithm"` // JWT signing algorithm (HS256, HS384, HS512, RS256, ES256). Default is "HS256"
}
+8 -1
View File
@@ -93,7 +93,7 @@ func setupFs() {
Config.Server.Filesystem.CreateDirectoryPermission = "755"
dirPermOctal, _ = strconv.ParseUint("755", 8, 32)
}
fileutils.SetFsPermissions(os.FileMode(filePermOctal), os.FileMode(dirPermOctal))
fileutils.SetFsPermissions(uint32(filePermOctal), uint32(dirPermOctal))
// Perform mandatory cache directory speed test
testCacheDirSpeed()
@@ -817,6 +817,13 @@ func loadCustomFavicon() {
Env.FaviconPath = validatedPath
Env.FaviconIsCustom = true
logger.Infof("Using custom favicon: %s", Env.FaviconPath)
// PWA and platform PNGs are generated at server startup from this path (SVG uses a raster
// sidecar next to the .svg when present; see icons.GeneratePWAIcons).
}
// PWAIconsCacheDir is where startup icon generation writes PNGs (under the server cache dir).
func PWAIconsCacheDir() string {
return filepath.Join(Config.Server.CacheDir, "icons")
}
func loadLoginIcon() {
+55 -41
View File
@@ -65,53 +65,67 @@ func AdminPerms() users.Permissions {
}
// Apply applies the default options to a user.
// Keep this in sync with [UserDefaults]: every user-facing field there should be copied
// here (except DefaultScopes, which is wired through source config rather than this helper).
func ApplyUserDefaults(u *users.User) {
u.StickySidebar = Config.UserDefaults.StickySidebar
u.DisableSettings = Config.UserDefaults.DisableSettings
u.HideFilesInTree = Config.UserDefaults.HideFilesInTree
u.DeleteAfterArchive = Config.UserDefaults.DeleteAfterArchive
d := Config.UserDefaults
// Handle DarkMode with default - dereference pointer from config
u.DarkMode = boolValueOrDefault(Config.UserDefaults.DarkMode, true)
u.DisableSettings = d.DisableSettings
u.LockPassword = d.LockPassword
u.Locale = Config.UserDefaults.Locale
u.ViewMode = Config.UserDefaults.ViewMode
u.SingleClick = Config.UserDefaults.SingleClick
u.EditorQuickSave = d.EditorQuickSave
u.HideSidebarFileActions = d.HideSidebarFileActions
u.DisableQuickToggles = d.DisableQuickToggles
u.DisableSearchOptions = d.DisableSearchOptions
u.StickySidebar = d.StickySidebar
u.HideFilesInTree = d.HideFilesInTree
u.DarkMode = boolValueOrDefault(d.DarkMode, true)
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick
u.ShowHidden = d.ShowHidden
u.DateFormat = d.DateFormat
u.GallerySize = d.GallerySize
u.ThemeColor = d.ThemeColor
u.QuickDownload = d.QuickDownload
u.DisablePreviewExt = d.DisablePreviewExt
u.DisableViewingExt = d.DisableViewingExt
u.DisableUpdateNotifications = d.DisableUpdateNotifications
u.DisableOfficePreviewExt = d.DisableOfficePreviewExt
u.DisableOnlyOfficeExt = d.DisableOnlyOfficeExt
u.CustomTheme = d.CustomTheme
u.ShowSelectMultiple = d.ShowSelectMultiple
u.DebugOffice = d.DebugOffice
u.DeleteWithoutConfirming = d.DeleteWithoutConfirming
u.DeleteAfterArchive = d.DeleteAfterArchive
u.PreferEditorForMarkdown = d.PreferEditorForMarkdown
u.FileLoading = d.FileLoading
// Handle Permissions - convert from pointer-based defaults to regular bools
u.Permissions.Api = Config.UserDefaults.Permissions.Api
u.Permissions.Admin = Config.UserDefaults.Permissions.Admin
u.Permissions.Modify = Config.UserDefaults.Permissions.Modify
u.Permissions.Share = Config.UserDefaults.Permissions.Share
u.Permissions.Realtime = Config.UserDefaults.Permissions.Realtime
u.Permissions.Delete = Config.UserDefaults.Permissions.Delete
u.Permissions.Create = Config.UserDefaults.Permissions.Create
u.Permissions.Download = boolValueOrDefault(Config.UserDefaults.Permissions.Download, true)
u.Permissions.Api = d.Permissions.Api
u.Permissions.Admin = d.Permissions.Admin
u.Permissions.Modify = d.Permissions.Modify
u.Permissions.Share = d.Permissions.Share
u.Permissions.Realtime = d.Permissions.Realtime
u.Permissions.Delete = d.Permissions.Delete
u.Permissions.Create = d.Permissions.Create
u.Permissions.Download = boolValueOrDefault(d.Permissions.Download, true)
// Handle Preview - convert from pointer-based defaults to regular bools
u.Preview.DisableHideSidebar = Config.UserDefaults.Preview.DisableHideSidebar
u.Preview.Image = boolValueOrDefault(Config.UserDefaults.Preview.Image, true)
u.Preview.Video = boolValueOrDefault(Config.UserDefaults.Preview.Video, true)
u.Preview.Audio = boolValueOrDefault(Config.UserDefaults.Preview.Audio, true)
u.Preview.MotionVideoPreview = boolValueOrDefault(Config.UserDefaults.Preview.MotionVideoPreview, true)
u.Preview.Office = boolValueOrDefault(Config.UserDefaults.Preview.Office, true)
u.Preview.PopUp = boolValueOrDefault(Config.UserDefaults.Preview.PopUp, true)
u.Preview.AutoplayMedia = boolValueOrDefault(Config.UserDefaults.Preview.AutoplayMedia, true)
u.Preview.DefaultMediaPlayer = Config.UserDefaults.Preview.DefaultMediaPlayer
u.Preview.Folder = boolValueOrDefault(Config.UserDefaults.Preview.Folder, true)
u.Preview.Models = boolValueOrDefault(Config.UserDefaults.Preview.Models, true)
u.Preview.DisableHideSidebar = d.Preview.DisableHideSidebar
u.Preview.Image = boolValueOrDefault(d.Preview.Image, true)
u.Preview.Video = boolValueOrDefault(d.Preview.Video, true)
u.Preview.Audio = boolValueOrDefault(d.Preview.Audio, true)
u.Preview.MotionVideoPreview = boolValueOrDefault(d.Preview.MotionVideoPreview, true)
u.Preview.Office = boolValueOrDefault(d.Preview.Office, true)
u.Preview.PopUp = boolValueOrDefault(d.Preview.PopUp, true)
u.Preview.AutoplayMedia = boolValueOrDefault(d.Preview.AutoplayMedia, true)
u.Preview.DefaultMediaPlayer = d.Preview.DefaultMediaPlayer
u.Preview.Folder = boolValueOrDefault(d.Preview.Folder, true)
u.Preview.Models = boolValueOrDefault(d.Preview.Models, true)
if u.LoginMethod == "" && d.LoginMethod != "" {
u.LoginMethod = users.LoginMethod(d.LoginMethod)
}
u.ShowHidden = Config.UserDefaults.ShowHidden
u.DateFormat = Config.UserDefaults.DateFormat
u.DisableViewingExt = Config.UserDefaults.DisableViewingExt
u.ThemeColor = Config.UserDefaults.ThemeColor
u.GallerySize = Config.UserDefaults.GallerySize
u.QuickDownload = Config.UserDefaults.QuickDownload
u.LockPassword = Config.UserDefaults.LockPassword
u.DisableOnlyOfficeExt = Config.UserDefaults.DisableOnlyOfficeExt
u.FileLoading = Config.UserDefaults.FileLoading
u.DisableOfficePreviewExt = Config.UserDefaults.DisableOfficePreviewExt
u.PreferEditorForMarkdown = Config.UserDefaults.PreferEditorForMarkdown
if len(u.Scopes) == 0 && u.Username != "anonymous" {
for _, source := range Config.Server.Sources {
if source.Config.DefaultEnabled {
-4
View File
@@ -36,10 +36,6 @@ type Environment struct {
FaviconPath string `json:"-"` // resolved favicon path (filesystem or embedded)
FaviconIsCustom bool `json:"-"` // true if favicon is from custom filesystem path
FaviconEmbeddedPath string `json:"-"` // embedded asset path for default favicon
PWAIconsDir string `json:"-"` // directory where generated PWA icons are stored
PWAIcon192 string `json:"-"` // path to 192x192 PWA icon
PWAIcon256 string `json:"-"` // path to 256x256 PWA icon
PWAIcon512 string `json:"-"` // path to 512x512 PWA icon
}
type Server struct {
+12 -7
View File
@@ -74,11 +74,6 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
return err
}
passwordUser := existingUser.LoginMethod == users.LoginMethodPassword
enforcedOtp := settings.Config.Auth.Methods.PasswordAuth.EnforcedOtp
if passwordUser && enforcedOtp && !existingUser.OtpEnabled {
return errors.ErrNoTotpConfigured
}
if user.LoginMethod == "" {
user.LoginMethod = existingUser.LoginMethod
}
@@ -86,11 +81,21 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
if err != nil {
return err
}
enforcedOtp := settings.Config.Auth.Methods.PasswordAuth.EnforcedOtp
if user.LoginMethod == "" {
user.LoginMethod = existingUser.LoginMethod
}
if user.TOTPNonce == "" {
user.TOTPNonce = existingUser.TOTPNonce
}
if user.TOTPSecret == "" {
user.TOTPSecret = existingUser.TOTPSecret
}
if user.LoginMethod == users.LoginMethodPassword && enforcedOtp && !user.OtpEnabled {
if slices.Contains(fields, "OtpEnabled") {
return errors.ErrNoTotpConfigured
}
}
if !slices.Contains(fields, "Password") {
user.Password = existingUser.Password
} else {
+1 -1
View File
@@ -83,7 +83,6 @@ type User struct {
LoginMethod LoginMethod `json:"loginMethod"`
OtpEnabled bool `json:"otpEnabled"` // true if TOTP is enabled, false otherwise
Version int `json:"version"`
ShowFirstLogin bool `json:"showFirstLogin"`
// legacy for migration purposes... og filebrowser has perm attribute
Perm Permissions `json:"perm,omitzero"` // deprecated: use Permissions instead
}
@@ -128,6 +127,7 @@ type NonAdminEditable struct {
HideFilesInTree bool `json:"hideFilesInTree"` // hide files in the sidebar tree navigation, when true, will show only directories.
DeleteAfterArchive bool `json:"deleteAfterArchive"` // delete source files after successful creation/extraction of archives
PreferEditorForMarkdown bool `json:"preferEditorForMarkdown"` // prefer editor first for markdown files instead of the Markdown Viewer
ShowFirstLogin bool `json:"showFirstLogin"`
}
type FileLoading struct {
+15 -15
View File
@@ -15,7 +15,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
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/gtsteffaniak/go-logger v1.1.0
github.com/kovidgoyal/imaging v1.8.21
github.com/mattn/go-sqlite3 v1.14.42
github.com/pquerna/otp v1.5.0
@@ -26,13 +26,13 @@ require (
golang.org/x/crypto v0.50.0
golang.org/x/image v0.39.0
golang.org/x/mod v0.35.0
golang.org/x/net v0.52.0
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.43.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.43.0
golang.org/x/tools v0.44.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.48.2
modernc.org/sqlite v1.49.1
)
require (
@@ -57,7 +57,7 @@ require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/air-verse/air v1.65.0 // indirect
github.com/air-verse/air v1.65.1 // indirect
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
@@ -114,16 +114,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.4 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // 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-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.26.0 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.26.0 // 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
@@ -139,7 +139,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-20260401045841-3289a7742193 // indirect
github.com/golang/geo v0.0.0-20260415063119-550b242b3150 // 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
@@ -281,7 +281,7 @@ require (
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.70.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
+40 -62
View File
@@ -75,18 +75,14 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/MirrexOne/unqueryvet v1.5.3 h1:LpT3rsH+IY3cQddWF9bg4C7jsbASdGnrOSofY8IPEiw=
github.com/MirrexOne/unqueryvet v1.5.3/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/air-verse/air v1.64.5 h1:+gs/NgTzYYe+gGPyfHy3XxpJReQWC1pIsiKIg0LgNt4=
github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
github.com/air-verse/air v1.65.0 h1:MCJTmXL+UBS1Ji05jsIaUBXokeW10W+Y6ODkx6GcfZk=
github.com/air-verse/air v1.65.0/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
github.com/air-verse/air v1.65.1 h1:G4hy4YemKrIcGAvYMor+86PhxdtBEq0n9ebJ4RoYy8w=
github.com/air-verse/air v1.65.1/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
@@ -104,8 +100,6 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
github.com/alexkohler/prealloc v1.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ=
github.com/alexkohler/prealloc v1.0.2/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
@@ -264,8 +258,6 @@ github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
@@ -314,33 +306,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.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
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.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-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
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=
@@ -409,8 +401,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-20260401045841-3289a7742193 h1:TWcQGetTogk7h5+TRKI6dnvXRQCtEdSPXLu4RdouuM8=
github.com/golang/geo v0.0.0-20260401045841-3289a7742193/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260415063119-550b242b3150 h1:F5CQANtNulclG+xJt370ohUujAEHq2XNBBAjkoSukQI=
github.com/golang/geo v0.0.0-20260415063119-550b242b3150/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=
@@ -450,8 +442,6 @@ github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarog
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint/v2 v2.10.1 h1:flhw5Px6ojbLyEFzXvJn5B2HEdkkRlkhE1SnmCbQBiE=
github.com/golangci/golangci-lint/v2 v2.10.1/go.mod h1:dBsrOk6zj0vDhlTv+IiJGqkDokR24IVTS7W3EVfPTQY=
github.com/golangci/golangci-lint/v2 v2.11.4 h1:GK+UlZBN5y7rh2PBnHA93XLSX6RaF7uhzJQ3JwU1wuA=
github.com/golangci/golangci-lint/v2 v2.11.4/go.mod h1:ODQDCASMA3VqfZYIbbQLpTRTzV7O/vjmIRF6u8NyFwI=
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
@@ -516,8 +506,8 @@ github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7bSKhkCE=
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
github.com/gtsteffaniak/go-logger v1.0.2 h1:S4AgR7yDu2geD8y2D9NyJVOBqpZRnl37r2mBi7GDNR0=
github.com/gtsteffaniak/go-logger v1.0.2/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
github.com/gtsteffaniak/go-logger v1.1.0 h1:baFds+iibuLLNpNLwygz32bDqdsunI6dGWcdxwMIVKI=
github.com/gtsteffaniak/go-logger v1.1.0/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
@@ -579,8 +569,6 @@ github.com/jupiterrider/ffi v0.6.0 h1:UX378KcZvH5c8qgLi9KL/bL82SZTHdRspZ+jj7bvBn
github.com/jupiterrider/ffi v0.6.0/go.mod h1:PqZ5Go6X9by8CIXgfprxfMPYmn8oT5m2O7AA56s64bY=
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=
github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -660,8 +648,6 @@ github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
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=
github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo=
github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -783,8 +769,6 @@ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
@@ -795,8 +779,6 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo=
github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0=
github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U=
github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
@@ -811,8 +793,6 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
@@ -880,8 +860,6 @@ github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSW
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
@@ -1041,8 +1019,8 @@ 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
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=
@@ -1194,8 +1172,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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
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=
@@ -1312,10 +1290,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
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.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
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=
@@ -1324,8 +1302,8 @@ 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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
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=
@@ -1334,8 +1312,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.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
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=
+20 -4
View File
@@ -147,6 +147,22 @@ type AuthTokenFrontend struct {
Permissions users.Permissions `json:"Permissions,omitempty"`
}
// authTokenIssuedUnix returns iat from JWT claims when set, otherwise the persisted int64 (e.g. loaded from DB).
func authTokenIssuedUnix(t users.AuthToken) int64 {
if t.RegisteredClaims.IssuedAt != nil {
return t.RegisteredClaims.IssuedAt.Unix()
}
return t.IssuedAt
}
// authTokenExpiresUnix returns exp from JWT claims when set, otherwise the persisted int64.
func authTokenExpiresUnix(t users.AuthToken) int64 {
if t.RegisteredClaims.ExpiresAt != nil {
return t.RegisteredClaims.ExpiresAt.Unix()
}
return t.ExpiresAt
}
// listApiTokensHandler lists all API tokens or retrieves details for a specific token.
// @Summary List API tokens
// @Description List all API tokens or retrieve details for a specific token.
@@ -169,8 +185,8 @@ func listApiTokensHandler(w http.ResponseWriter, r *http.Request, d *requestCont
AuthTokensFrontend = append(AuthTokensFrontend, AuthTokenFrontend{
Token: token.Token,
Name: name,
IssuedAt: token.RegisteredClaims.IssuedAt.Unix(),
ExpiresAt: token.RegisteredClaims.ExpiresAt.Unix(),
IssuedAt: authTokenIssuedUnix(token),
ExpiresAt: authTokenExpiresUnix(token),
Permissions: token.Permissions,
})
}
@@ -205,8 +221,8 @@ func getApiTokenHandler(w http.ResponseWriter, r *http.Request, d *requestContex
AuthTokenFrontendResponse := AuthTokenFrontend{
Token: tokenInfo.Token,
Name: name,
IssuedAt: tokenInfo.IssuedAt,
ExpiresAt: tokenInfo.ExpiresAt,
IssuedAt: authTokenIssuedUnix(tokenInfo),
ExpiresAt: authTokenExpiresUnix(tokenInfo),
Permissions: tokenInfo.Permissions,
}
return renderJSON(w, r, AuthTokenFrontendResponse)
+1
View File
@@ -219,6 +219,7 @@ func fileWatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
return http.StatusInternalServerError, fmt.Errorf("error checking file type: %v", err)
}
if isText {
response.IsText = true
// Read the last N lines for text files only
content, err := readLastNLines(realPath, lines)
if err != nil {
+3 -3
View File
@@ -92,11 +92,11 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
// ========================================
// Auth Routes - /api/auth/
// ========================================
api.HandleFunc("POST /auth/login", userWithoutOTP(loginHandler))
api.HandleFunc("POST /auth/login", userWithoutOTP(false, loginHandler))
api.HandleFunc("POST /auth/logout", withOrWithoutUser(logoutHandler))
api.HandleFunc("POST /auth/signup", withoutUser(signupHandler))
api.HandleFunc("POST /auth/otp/generate", userWithoutOTP(generateOTPHandler))
api.HandleFunc("POST /auth/otp/verify", userWithoutOTP(verifyOTPHandler))
api.HandleFunc("POST /auth/otp/generate", userWithoutOTP(true, generateOTPHandler))
api.HandleFunc("POST /auth/otp/verify", userWithoutOTP(false, verifyOTPHandler))
api.HandleFunc("POST /auth/renew", withUser(renewHandler))
api.HandleFunc("POST /auth/token", withUser(createApiTokenHandler))
api.HandleFunc("DELETE /auth/token", withUser(deleteApiTokenHandler))
+10 -6
View File
@@ -314,7 +314,7 @@ func withoutUserHelper(fn handleFunc) handleFunc {
}
// allow user without OTP to pass
func userWithoutOTPhelper(fn handleFunc) handleFunc {
func userWithoutOTPhelper(disableOtp bool, fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if config.Auth.Methods.ProxyAuth.Enabled {
@@ -344,15 +344,19 @@ func userWithoutOTPhelper(fn handleFunc) handleFunc {
logger.Debug("ldap auth failed, calling password auth", err)
}
if config.Auth.Methods.PasswordAuth.Enabled {
// Get the authentication method from the settings
auther, err := store.Auth.Get("password")
if err != nil {
return 401, errors.ErrUnauthorized
}
// Authenticate the user based on the request
// OTP routes: password only (do not mutate stored *JSONAuth).
if ja, ok := auther.(*auth.JSONAuth); ok {
x := *ja
x.DisableOtp = disableOtp
auther = &x
}
user, err := auther.Auth(r, store.Users)
if err != nil {
logger.Debug("password auth failed, calling handler", err)
logger.Debug("password auth failed, calling handler:", err)
if err == errors.ErrNoTotpProvided {
return 403, err
}
@@ -655,8 +659,8 @@ func withoutUser(fn handleFunc) http.HandlerFunc {
return wrapHandler(withoutUserHelper(fn))
}
func userWithoutOTP(fn handleFunc) http.HandlerFunc {
return wrapHandler(userWithoutOTPhelper(fn))
func userWithoutOTP(disableOtp bool, fn handleFunc) http.HandlerFunc {
return wrapHandler(userWithoutOTPhelper(disableOtp, fn))
}
func withSelfOrAdmin(fn handleFunc) http.HandlerFunc {
+4
View File
@@ -521,6 +521,10 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
existingIsDir := stat.IsDir()
requestingDir := isDir
if r.URL.Query().Get("override") != "true" {
return http.StatusConflict, nil
}
// If type mismatch (file vs folder or folder vs file) and not overriding
if existingIsDir != requestingDir && r.URL.Query().Get("override") != "true" {
logger.Debugf("Type conflict detected in chunked: existing is dir=%v, requesting dir=%v at path=%v", existingIsDir, requestingDir, realPath)
+13 -34
View File
@@ -83,7 +83,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
staticURL := config.Server.BaseURL + "public/static"
description := config.Frontend.Description
title := config.Frontend.Name
banner := staticURL + "/pwa-icon-512.png" // Use largest generated icon for best quality
banner := staticURL + "/icons/pwa-icon-512.png" // largest generated PWA icon
disableSidebar := false
// Use custom favicon if configured and validated, otherwise fall back to default
@@ -139,9 +139,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
// Determine OpenGraph image: use banner if set, otherwise use largest available icon (512x512)
ogImage := banner
if banner == staticURL+"/pwa-icon-512.png" {
if banner == staticURL+"/icons/pwa-icon-512.png" {
// Note: 512x512 is square; OpenGraph prefers 1200x630 (1.91:1 ratio) but square works fine
ogImage = staticURL + "/pwa-icon-512.png"
ogImage = staticURL + "/icons/pwa-icon-512.png"
}
// Construct the full URL for the current request
@@ -158,22 +158,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
fullURL = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.Path)
}
// Determine PWA icon URLs based on custom favicon settings
pwaIcon192 := staticURL + "/" + settings.Env.PWAIcon192
pwaIcon256 := staticURL + "/" + settings.Env.PWAIcon256
pwaIcon512 := staticURL + "/" + settings.Env.PWAIcon512
// If custom favicon is set and it's SVG, use the favicon directly
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) == ".svg" {
pwaIcon192 = favicon
pwaIcon256 = favicon
pwaIcon512 = favicon
} else if settings.Env.FaviconIsCustom {
// For custom PNG/ICO favicons, use generated PWA icons
pwaIcon192 = staticURL + "/pwa-icon-192.png"
pwaIcon256 = staticURL + "/pwa-icon-256.png"
pwaIcon512 = staticURL + "/pwa-icon-512.png"
}
pwaIcon192 := staticURL + "/icons/pwa-icon-192.png"
pwaIcon256 := staticURL + "/icons/pwa-icon-256.png"
pwaIcon512 := staticURL + "/icons/pwa-icon-512.png"
data["htmlVars"] = map[string]interface{}{
"title": title,
@@ -186,8 +173,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"favicon": favicon,
"loginIcon": loginIcon,
"color": defaultThemeColor,
"winIcon": staticURL + "/mstile-256x256.png",
"appIcon": staticURL + "/apple-touch-icon.png",
"winIcon": staticURL + "/icons/mstile-256x256.png",
"appIcon": staticURL + "/icons/apple-touch-icon.png",
"description": description,
"loadingSpinnersCSS": template.CSS(loadingSpinnersCSS),
"banner": banner,
@@ -313,7 +300,7 @@ func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
assetPath = settings.Env.FaviconEmbeddedPath
case "favicon.png":
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) != ".svg" {
iconPath := filepath.Join(settings.Env.PWAIconsDir, "pwa-icon-512.png")
iconPath := filepath.Join(settings.PWAIconsCacheDir(), "pwa-icon-512.png")
if _, err := os.Stat(iconPath); err == nil {
http.ServeFile(w, r, iconPath)
return
@@ -323,18 +310,10 @@ func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
assetPath = "img/icons/favicon.png"
case "icons/favicon-32x32.png",
"icons/pwa-icon-192.png", "icons/pwa-icon-256.png", "icons/pwa-icon-512.png",
"icons/apple-touch-icon.png":
// Serve generated icons from cache directory
iconPath := filepath.Join(settings.Env.PWAIconsDir, path)
if _, err := os.Stat(iconPath); err == nil {
http.ServeFile(w, r, iconPath)
return
}
// Fall back to embedded favicon.png if generation failed
assetPath = "img/icons/favicon.png"
case "icons/mstile-256x256.png":
// Windows tile - redirect to pwa-icon-256.png (they're identical)
iconPath := filepath.Join(settings.Env.PWAIconsDir, "pwa-icon-256.png")
"icons/apple-touch-icon.png", "icons/mstile-256x256.png":
// Files are generated as PWAIconsCacheDir()/basename (URL uses icons/ prefix only for routing)
rel := strings.TrimPrefix(path, "icons/")
iconPath := filepath.Join(settings.PWAIconsCacheDir(), rel)
if _, err := os.Stat(iconPath); err == nil {
http.ServeFile(w, r, iconPath)
return
+40 -21
View File
@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/url"
"reflect"
"slices"
"sort"
"strconv"
@@ -257,25 +258,43 @@ func usersPostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
return http.StatusCreated, nil
}
// passwordUpdateRequested reports whether the client intends to change the target user's password
// (non-empty new password and "password" in the which list). This matches storage parseFields behavior.
func passwordUpdateRequested(req *UserRequest) bool {
if req.User.Password == "" {
return false
// nonAdminEditableFieldNameSet returns User.NonAdminEditable struct field names (e.g. "Locale", "Preview").
func nonAdminEditableFieldNameSet() map[string]struct{} {
m := make(map[string]struct{})
t := reflect.TypeOf(users.NonAdminEditable{})
for i := 0; i < t.NumField(); i++ {
m[t.Field(i).Name] = struct{}{}
}
for _, w := range req.Which {
if strings.EqualFold(w, "password") {
return true
}
}
return false
return m
}
// verifyActorPasswordForUserPasswordChange ensures the authenticated actor re-proves their password
// when changing another user's or their own password via PUT. Requires URL-encoded X-Password (same as login).
func verifyActorPasswordForUserPasswordChange(r *http.Request, d *requestContext, target *users.User) (int, error) {
if d.user.LoginMethod != users.LoginMethodPassword || target.LoginMethod != users.LoginMethodPassword {
return http.StatusForbidden, fmt.Errorf("password can only be changed when both accounts use password login")
// userPutOnlyNonAdminEditableFields reports whether req.Which lists exclusively NonAdminEditable fields,
// excluding Password. Empty which or which[0] == "all" means a broad update and returns false.
func userPutOnlyNonAdminEditableFields(which []string) bool {
if len(which) == 0 || strings.EqualFold(strings.TrimSpace(which[0]), "all") {
return false
}
allowed := nonAdminEditableFieldNameSet()
for _, w := range which {
f := utils.CapitalizeFirst(strings.TrimSpace(w))
if f == "" {
return false
}
if strings.EqualFold(f, "Password") {
return false
}
if _, ok := allowed[f]; !ok {
return false
}
}
return true
}
// verifyActorPasswordForUserPut requires URL-encoded X-Password when the authenticated actor uses
// password login. Callers should invoke this only when the update requires re-authentication.
func verifyActorPasswordForUserPut(r *http.Request, d *requestContext) (int, error) {
if d.user.LoginMethod != users.LoginMethodPassword {
return 0, nil
}
encoded := r.Header.Get("X-Password")
if encoded == "" {
@@ -300,16 +319,16 @@ func verifyActorPasswordForUserPasswordChange(r *http.Request, d *requestContext
// userPutHandler updates an existing user's details.
// @Summary Update a user's details
// @Description Updates the details of a user identified by ID. When updating the target user's password the actor must send their current password in the X-Password header
// @Description Updates the details of a user identified by ID. When the authenticated actor uses password login, they must send their current password in the X-Password header unless the update only touches NonAdminEditable profile fields (not password). Full updates (which empty or "all") or any admin-only field require confirmation.
// @Tags Users
// @Accept json
// @Param id query string false "user ID to update"
// @Param id query string false "usename to update"
// @Param X-Password header string false "Actor's current password (URL-encoded); required when changing a password user's password"
// @Param X-Password header string false "Actor's current password (URL-encoded); required for password-login actors when updating password, using which=all, or any field outside NonAdminEditable"
// @Param data body users.User true "User data to update"
// @Success 204 "No Content - User updated successfully"
// @Failure 400 {object} map[string]string "Bad Request"
// @Failure 401 {object} map[string]string "Unauthorized - invalid or missing actor password for password change"
// @Failure 401 {object} map[string]string "Unauthorized - invalid or missing actor password when required"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /api/users [put]
@@ -361,9 +380,9 @@ func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
return http.StatusBadRequest, fmt.Errorf("failed to get user: %w", err)
}
if passwordUpdateRequested(&req) {
if d.user.LoginMethod == users.LoginMethodPassword && !userPutOnlyNonAdminEditableFields(req.Which) {
var status int
status, err = verifyActorPasswordForUserPasswordChange(r, d, oldUser)
status, err = verifyActorPasswordForUserPut(r, d)
if err != nil {
return status, err
}
+2
View File
@@ -24,6 +24,8 @@ func errToStatus(err error) int {
return http.StatusForbidden
case errors.Is(err, libErrors.ErrInvalidRequestParams):
return http.StatusBadRequest
case errors.Is(err, libErrors.ErrIsDirectory):
return http.StatusMethodNotAllowed
default:
return http.StatusInternalServerError
}
+9 -21
View File
@@ -29,7 +29,6 @@ const (
type iconSize struct {
size int
name string
env *string
}
// determineSourceType determines which type of favicon source we have
@@ -153,13 +152,13 @@ func handleRasterFavicon(faviconPath string) ([]byte, error) {
func getIconSizesToGenerate() []iconSize {
return []iconSize{
// Browser favicon (32x32 PNG for Safari/older browsers)
{32, "favicon-32x32.png", nil},
{32, "favicon-32x32.png"},
// PWA icons (PNG required for Apple PWA compatibility)
{192, "pwa-icon-192.png", &settings.Env.PWAIcon192},
{256, "pwa-icon-256.png", &settings.Env.PWAIcon256}, // Also serves as Windows tile
{512, "pwa-icon-512.png", &settings.Env.PWAIcon512},
{192, "pwa-icon-192.png"},
{256, "pwa-icon-256.png"}, // Also serves as Windows tile
{512, "pwa-icon-512.png"},
// Platform-specific icons
{180, "apple-touch-icon.png", nil}, // iOS home screen (required for Apple)
{180, "apple-touch-icon.png"}, // iOS home screen (required for Apple)
}
}
@@ -181,10 +180,6 @@ func generateIconSizes(sourceData []byte) error {
continue
}
// Update environment variable if specified
if icon.env != nil {
*icon.env = filepath.Join("icons", icon.name)
}
generatedCount++
}
@@ -199,7 +194,7 @@ func generateIconSizes(sourceData []byte) error {
// generateSingleIcon generates a single icon file at the specified size
func generateSingleIcon(previewService *preview.Service, sourceData []byte, icon iconSize) error {
outputPath := filepath.Join(settings.Env.PWAIconsDir, icon.name)
outputPath := filepath.Join(settings.PWAIconsCacheDir(), icon.name)
// Create output file
outFile, err := os.Create(outputPath)
@@ -233,18 +228,11 @@ func generateSingleIcon(previewService *preview.Service, sourceData []byte, icon
// All icons are generated as PNG for maximum compatibility (Apple devices, PWA, older browsers)
func GeneratePWAIcons() error {
logger.Info("Generating PWA icons...")
// Set PWA icons directory in cache
settings.Env.PWAIconsDir = filepath.Join(settings.Config.Server.CacheDir, "icons")
// Initialize with default paths (all icons will be generated)
settings.Env.PWAIcon192 = "icons/pwa-icon-192.png"
settings.Env.PWAIcon256 = "icons/pwa-icon-256.png"
settings.Env.PWAIcon512 = "icons/pwa-icon-512.png"
cacheDir := settings.PWAIconsCacheDir()
// Create icons directory with configured permissions
// Note: Parent cache directory should already exist from testCacheDirSpeed()
if err := os.MkdirAll(settings.Env.PWAIconsDir, fileutils.PermDir); err != nil {
logger.Warningf("Failed to create PWA icons directory %s: %v", settings.Env.PWAIconsDir, err)
if err := os.MkdirAll(cacheDir, fileutils.PermDir); err != nil {
logger.Warningf("Failed to create PWA icons directory %s: %v", cacheDir, err)
}
// Load and prepare source data as PNG
sourceData, err := loadFaviconSource()
+9 -19
View File
@@ -1,9 +1,6 @@
package icons
import (
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
)
@@ -36,6 +33,9 @@ func generatePWAManifest(name, description, baseURL, themeColor, pwaIcon192, pwa
if len(name) > 12 {
shortName = name[:12]
}
if name == "FileBrowser Quantum" {
shortName = "FBQ"
}
return PWAManifest{
Name: name,
@@ -68,7 +68,9 @@ func generatePWAManifest(name, description, baseURL, themeColor, pwaIcon192, pwa
}
}
// InitializePWAManifest generates and caches the PWA manifest at startup
// InitializePWAManifest caches the PWA manifest at startup. Icon URLs always point at
// PNGs under public/static/icons/, which GeneratePWAIcons produces from the configured
// favicon (custom SVG uses a raster sidecar as the raster source).
func InitializePWAManifest() {
config := &settings.Config
staticURL := config.Server.BaseURL + "public/static"
@@ -76,21 +78,9 @@ func InitializePWAManifest() {
description := config.Frontend.Description
defaultThemeColor := "#455a64"
// Determine PWA icon URLs based on custom favicon settings
pwaIcon192 := staticURL + "/" + settings.Env.PWAIcon192
pwaIcon256 := staticURL + "/" + settings.Env.PWAIcon256
pwaIcon512 := staticURL + "/" + settings.Env.PWAIcon512
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) == ".svg" {
favicon := staticURL + "/favicon"
pwaIcon192 = favicon
pwaIcon256 = favicon
pwaIcon512 = favicon
} else if settings.Env.FaviconIsCustom {
pwaIcon192 = staticURL + "/icons/pwa-icon-192.png"
pwaIcon256 = staticURL + "/icons/pwa-icon-256.png"
pwaIcon512 = staticURL + "/icons/pwa-icon-512.png"
}
pwaIcon192 := staticURL + "/icons/pwa-icon-192.png"
pwaIcon256 := staticURL + "/icons/pwa-icon-256.png"
pwaIcon512 := staticURL + "/icons/pwa-icon-512.png"
CachedManifest = generatePWAManifest(title, description, config.Server.BaseURL, defaultThemeColor, pwaIcon192, pwaIcon256, pwaIcon512)
}
+10 -29
View File
@@ -85,17 +85,17 @@ type Stats struct {
LastIndexedUnix int64 `json:"lastIndexedUnixTime"`
Complexity uint `json:"complexity"`
LastScanned time.Time `json:"lastScanned"`
DiskUsed uint64 `json:"used"`
UsedAlt uint64 `json:"usedAlt"`
UsedAsIndexed uint64 `json:"used"`
UsedDisk uint64 `json:"usedAlt"`
DiskTotal uint64 `json:"total"`
}
// reduced index is json exposed to the client
type ReducedIndex struct {
Stats
IdxName string `json:"name"`
DiskTotal uint64 `json:"total"`
Status IndexStatus `json:"status"`
Scanners []*ScannerInfo `json:"scanners,omitempty"`
IdxName string `json:"name"`
Status IndexStatus `json:"status"`
Scanners []*ScannerInfo `json:"scanners,omitempty"`
}
type Index struct {
@@ -1158,22 +1158,6 @@ func (idx *Index) updateFolderSizeAndParents(path string, newSize uint64, previo
}
}
// SetFolderSize sets the size for a directory in the in-memory map
func (idx *Index) SetFolderSize(path string, newSize uint64) {
idx.folderSizesMu.Lock()
defer idx.folderSizesMu.Unlock()
idx.folderSizes[path] = newSize
idx.folderSizesUnsynced[path] = struct{}{} // Mark as changed for next DB sync
}
// GetFolderSize retrieves the size for a directory from the in-memory map
func (idx *Index) GetFolderSize(path string) (uint64, bool) {
idx.folderSizesMu.RLock()
defer idx.folderSizesMu.RUnlock()
size, exists := idx.folderSizes[path]
return size, exists
}
// GetFolderSizeForDisplay retrieves folder size with appropriate formatting based on config
func (idx *Index) GetFolderSizeForDisplay(path string) int64 {
size, exists := idx.GetFolderSize(path)
@@ -1532,18 +1516,15 @@ func (idx *Index) IsNeverWatchPath(adjustedPath string) bool {
return exists
}
type DiskUsage struct {
Total uint64 `json:"total"`
Used uint64 `json:"used"`
}
func (idx *Index) SetUsage(totalBytes uint64) {
func (idx *Index) SetUsage(totalDiskSize, partitionUsed, UsedAsIndexed uint64) {
if settings.Config.Frontend.DisableUsedPercentage {
return
}
idx.mu.Lock()
defer idx.mu.Unlock()
idx.DiskTotal = totalBytes
idx.DiskTotal = totalDiskSize
idx.UsedAsIndexed = UsedAsIndexed
idx.UsedDisk = partitionUsed
}
func (idx *Index) SetStatus(status IndexStatus) error {
-10
View File
@@ -231,16 +231,6 @@ func (idx *Index) setupMultiScanner(isNewDb bool) {
logger.Errorf("Failed to load persisted index data for [%v]: %v", idx.Name, err)
}
// Load existing folder sizes from database to avoid marking everything as dirty on startup
existingSizes, err := idx.db.LoadFolderSizes(idx.Name)
if err != nil {
logger.Errorf("[INIT] Failed to load existing folder sizes for [%s]: %v", idx.Name, err)
} else {
idx.folderSizesMu.Lock()
idx.folderSizes = existingSizes
idx.folderSizesMu.Unlock()
}
idx.mu.Lock()
idx.scanners = make(map[string]*Scanner)
idx.mu.Unlock()
+32 -26
View File
@@ -296,8 +296,8 @@ func GetIndexInfo(sourceName string, forceCacheRefresh bool) (ReducedIndex, erro
return ReducedIndex{}, fmt.Errorf("index %s not found", sourceName)
}
// Only update disk total if cache is missing or explicitly forced
// The "used" value comes from totalSize and is always current
// Only update disk total if cache is missing or explicitly forced.
// Indexed "used" bytes for the UI come from the SQLite root row (see below), not in-memory scan state.
sourcePath := idx.Path
cacheKey := "usageCache-" + sourceName
if forceCacheRefresh {
@@ -307,26 +307,24 @@ func GetIndexInfo(sourceName string, forceCacheRefresh bool) (ReducedIndex, erro
_, ok = utils.DiskUsageCache.Get(cacheKey)
if !ok {
// Only fetch disk total and used if not cached (this is expensive, so we cache it)
totalBytes, err := fileutils.GetPartitionSize(sourcePath)
totalPartitionSize, err := fileutils.GetPartitionSize(sourcePath)
if err != nil {
idx.mu.Lock()
idx.Status = UNAVAILABLE
idx.mu.Unlock()
return ReducedIndex{}, fmt.Errorf("error getting disk usage for %s: %v", sourcePath, err)
logger.Errorf("Failed to get partition size for index %s: %v", sourceName, err)
}
itemInfo, dbErr := idx.db.GetDirectoryChildren(idx.Name, "/")
if dbErr != nil {
logger.Debugf("GetIndexInfo: could not read root folder size from index DB for %s: %v", sourceName, dbErr)
}
indexedSizeFromDB := uint64(0)
for _, item := range itemInfo {
indexedSizeFromDB += uint64(item.Size)
}
idx.SetUsage(totalBytes)
// Also fetch OS-reported partition used space (total - free)
usedAlt, err := fileutils.GetPartitionUsed(sourcePath)
partitionUsed, err := fileutils.GetPartitionUsed(sourcePath)
if err != nil {
logger.Errorf("Failed to get partition used space for index %s: %v", sourceName, err)
usedAlt = 0
}
idx.mu.Lock()
idx.UsedAlt = usedAlt
idx.mu.Unlock()
idx.SetUsage(totalPartitionSize, partitionUsed, indexedSizeFromDB)
utils.DiskUsageCache.Set(cacheKey, true)
}
@@ -348,16 +346,8 @@ func GetIndexInfo(sourceName string, forceCacheRefresh bool) (ReducedIndex, erro
})
}
reducedIdx := idx.ReducedIndex
var diskUsed uint64
rootTotal, ok := idx.GetFolderSize("/")
if !ok {
logger.Errorf("Failed to get root-level indexed total for index %s", sourceName)
diskUsed = 0
} else {
diskUsed = rootTotal
}
reducedIdx.DiskUsed = diskUsed
reducedIdx.UsedAlt = idx.UsedAlt
reducedIdx.UsedAsIndexed = idx.UsedAsIndexed
reducedIdx.UsedDisk = idx.UsedDisk
reducedIdx.DiskTotal = idx.DiskTotal
reducedIdx.Scanners = scannerInfos
reducedIdx.NumDirs = idx.getNumDirsUnlocked()
@@ -372,3 +362,19 @@ func GetIndexInfo(sourceName string, forceCacheRefresh bool) (ReducedIndex, erro
idx.mu.RUnlock()
return reducedIdx, nil
}
// SetFolderSize sets the size for a directory in the in-memory map
func (idx *Index) SetFolderSize(path string, newSize uint64) {
idx.folderSizesMu.Lock()
defer idx.folderSizesMu.Unlock()
idx.folderSizes[path] = newSize
idx.folderSizesUnsynced[path] = struct{}{} // Mark as changed for next DB sync
}
// GetFolderSize retrieves the size for a directory from the in-memory map
func (idx *Index) GetFolderSize(path string) (uint64, bool) {
idx.folderSizesMu.RLock()
defer idx.folderSizesMu.RUnlock()
size, exists := idx.folderSizes[path]
return size, exists
}
+2 -2
View File
@@ -1,6 +1,6 @@
.card,
#listingView.gallery .item,
#listingView.normal .item,
.listing-item,
.floating-window,
.button,
button,
.input,
+4 -4
View File
@@ -3076,7 +3076,7 @@ const docTemplate = `{
}
},
"put": {
"description": "Updates the details of a user identified by ID. When updating the target user's password the actor must send their current password in the X-Password header",
"description": "Updates the details of a user identified by ID. When the authenticated actor uses password login, they must send their current password in the X-Password header unless the update only touches NonAdminEditable profile fields (not password). Full updates (which empty or \"all\") or any admin-only field require confirmation.",
"consumes": [
"application/json"
],
@@ -3099,7 +3099,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "Actor's current password (URL-encoded); required when changing a password user's password",
"description": "Actor's current password (URL-encoded); required for password-login actors when updating password, using which=all, or any field outside NonAdminEditable",
"name": "X-Password",
"in": "header"
},
@@ -3127,7 +3127,7 @@ const docTemplate = `{
}
},
"401": {
"description": "Unauthorized - invalid or missing actor password for password change",
"description": "Unauthorized - invalid or missing actor password when required",
"schema": {
"type": "object",
"additionalProperties": {
@@ -5252,7 +5252,7 @@ const docTemplate = `{
"type": "string"
},
"secret": {
"description": "secret: shared secret key for verifying JWT token signatures (required)",
"description": "Shared secret key/bytes for verifying JWT token signatures (required, eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)",
"type": "string"
},
"userGroups": {
+4 -4
View File
@@ -3065,7 +3065,7 @@
}
},
"put": {
"description": "Updates the details of a user identified by ID. When updating the target user's password the actor must send their current password in the X-Password header",
"description": "Updates the details of a user identified by ID. When the authenticated actor uses password login, they must send their current password in the X-Password header unless the update only touches NonAdminEditable profile fields (not password). Full updates (which empty or \"all\") or any admin-only field require confirmation.",
"consumes": [
"application/json"
],
@@ -3088,7 +3088,7 @@
},
{
"type": "string",
"description": "Actor's current password (URL-encoded); required when changing a password user's password",
"description": "Actor's current password (URL-encoded); required for password-login actors when updating password, using which=all, or any field outside NonAdminEditable",
"name": "X-Password",
"in": "header"
},
@@ -3116,7 +3116,7 @@
}
},
"401": {
"description": "Unauthorized - invalid or missing actor password for password change",
"description": "Unauthorized - invalid or missing actor password when required",
"schema": {
"type": "object",
"additionalProperties": {
@@ -5241,7 +5241,7 @@
"type": "string"
},
"secret": {
"description": "secret: shared secret key for verifying JWT token signatures (required)",
"description": "Shared secret key/bytes for verifying JWT token signatures (required, eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)",
"type": "string"
},
"userGroups": {
+10 -9
View File
@@ -801,8 +801,8 @@ definitions:
to logout url. Custom logout query params are respected.
type: string
secret:
description: 'secret: shared secret key for verifying JWT token signatures
(required)'
description: Shared secret key/bytes for verifying JWT token signatures (required,
eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)
type: string
userGroups:
description: if set, only users in these groups are allowed to log in. Blocks
@@ -4237,9 +4237,11 @@ paths:
put:
consumes:
- application/json
description: Updates the details of a user identified by ID. When updating the
target user's password the actor must send their current password in the X-Password
header
description: Updates the details of a user identified by ID. When the authenticated
actor uses password login, they must send their current password in the X-Password
header unless the update only touches NonAdminEditable profile fields (not
password). Full updates (which empty or "all") or any admin-only field require
confirmation.
parameters:
- description: user ID to update
in: query
@@ -4249,8 +4251,8 @@ paths:
in: query
name: id
type: string
- description: Actor's current password (URL-encoded); required when changing
a password user's password
- description: Actor's current password (URL-encoded); required for password-login
actors when updating password, using which=all, or any field outside NonAdminEditable
in: header
name: X-Password
type: string
@@ -4270,8 +4272,7 @@ paths:
type: string
type: object
"401":
description: Unauthorized - invalid or missing actor password for password
change
description: Unauthorized - invalid or missing actor password when required
schema:
additionalProperties:
type: string
+130 -169
View File
@@ -11,7 +11,6 @@
"@onlyoffice/document-editor-vue": "^1.4.0",
"ace-builds": "^1.24.2",
"axios": "^1.13.5",
"clipboard": "^2.0.4",
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
@@ -1166,9 +1165,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
"integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
"cpu": [
"arm"
],
@@ -1180,9 +1179,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
"integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
"cpu": [
"arm64"
],
@@ -1194,9 +1193,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
"integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
"cpu": [
"arm64"
],
@@ -1208,9 +1207,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
"integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
"cpu": [
"x64"
],
@@ -1222,9 +1221,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
"integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
"cpu": [
"arm64"
],
@@ -1236,9 +1235,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
"integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
"cpu": [
"x64"
],
@@ -1250,9 +1249,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
"integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
"cpu": [
"arm"
],
@@ -1264,9 +1263,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
"integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
"cpu": [
"arm"
],
@@ -1278,9 +1277,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
"integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
"cpu": [
"arm64"
],
@@ -1292,9 +1291,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
"integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
"cpu": [
"arm64"
],
@@ -1306,9 +1305,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
"integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
"cpu": [
"loong64"
],
@@ -1320,9 +1319,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
"integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
"cpu": [
"loong64"
],
@@ -1334,9 +1333,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
"integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
"cpu": [
"ppc64"
],
@@ -1348,9 +1347,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
"integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
"cpu": [
"ppc64"
],
@@ -1362,9 +1361,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
"integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
"cpu": [
"riscv64"
],
@@ -1376,9 +1375,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
"integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
"cpu": [
"riscv64"
],
@@ -1390,9 +1389,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
"integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
"cpu": [
"s390x"
],
@@ -1404,9 +1403,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
"integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
"cpu": [
"x64"
],
@@ -1418,9 +1417,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
"cpu": [
"x64"
],
@@ -1432,9 +1431,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
"integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
"cpu": [
"x64"
],
@@ -1446,9 +1445,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
"integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
"cpu": [
"arm64"
],
@@ -1460,9 +1459,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
"integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
"cpu": [
"arm64"
],
@@ -1474,9 +1473,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
"integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
"cpu": [
"ia32"
],
@@ -1488,9 +1487,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
"integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
"cpu": [
"x64"
],
@@ -1502,9 +1501,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
"integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
"cpu": [
"x64"
],
@@ -2127,9 +2126,9 @@
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.12",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -2268,9 +2267,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
"integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
@@ -2444,17 +2443,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"license": "MIT",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2659,9 +2647,9 @@
"license": "MIT"
},
"node_modules/deepl-node": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.25.0.tgz",
"integrity": "sha512-vOdPNZCegDS9F2GR8iwsEL+2w+Io9LBO0iECuZqWQMePZMNeNuODs6cRaiPUmVobPr6Dkt3HKiAAlZqFfl7vzw==",
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.26.0.tgz",
"integrity": "sha512-KaMZ248O/N2HrevQnsJoCoHxDUNBbsQ7+OZJa2YAOhym4qtVCNTzRst+4rLX9B9T84SVN4xVQSuJyFIdlI/x1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2702,12 +2690,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"license": "MIT"
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
@@ -2741,9 +2723,9 @@
}
},
"node_modules/dompurify": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
"integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -3493,9 +3475,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -3709,15 +3691,6 @@
"node": ">= 4"
}
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3782,9 +3755,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -4169,9 +4142,9 @@
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4885,9 +4858,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"funding": [
{
"type": "opencollective",
@@ -5087,9 +5060,9 @@
}
},
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
"integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5103,31 +5076,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"@rollup/rollup-android-arm-eabi": "4.60.2",
"@rollup/rollup-android-arm64": "4.60.2",
"@rollup/rollup-darwin-arm64": "4.60.2",
"@rollup/rollup-darwin-x64": "4.60.2",
"@rollup/rollup-freebsd-arm64": "4.60.2",
"@rollup/rollup-freebsd-x64": "4.60.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
"@rollup/rollup-linux-arm-musleabihf": "4.60.2",
"@rollup/rollup-linux-arm64-gnu": "4.60.2",
"@rollup/rollup-linux-arm64-musl": "4.60.2",
"@rollup/rollup-linux-loong64-gnu": "4.60.2",
"@rollup/rollup-linux-loong64-musl": "4.60.2",
"@rollup/rollup-linux-ppc64-gnu": "4.60.2",
"@rollup/rollup-linux-ppc64-musl": "4.60.2",
"@rollup/rollup-linux-riscv64-gnu": "4.60.2",
"@rollup/rollup-linux-riscv64-musl": "4.60.2",
"@rollup/rollup-linux-s390x-gnu": "4.60.2",
"@rollup/rollup-linux-x64-gnu": "4.60.2",
"@rollup/rollup-linux-x64-musl": "4.60.2",
"@rollup/rollup-openbsd-x64": "4.60.2",
"@rollup/rollup-openharmony-arm64": "4.60.2",
"@rollup/rollup-win32-arm64-msvc": "4.60.2",
"@rollup/rollup-win32-ia32-msvc": "4.60.2",
"@rollup/rollup-win32-x64-gnu": "4.60.2",
"@rollup/rollup-win32-x64-msvc": "4.60.2",
"fsevents": "~2.3.2"
}
},
@@ -5188,12 +5161,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -5507,12 +5474,6 @@
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT"
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -5693,9 +5654,9 @@
}
},
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
-1
View File
@@ -25,7 +25,6 @@
"@onlyoffice/document-editor-vue": "^1.4.0",
"ace-builds": "^1.24.2",
"axios": "^1.13.5",
"clipboard": "^2.0.4",
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
+1 -1
View File
@@ -115,7 +115,7 @@ auth:
disableVerifyTLS: false # disable TLS verification (insecure, for testing only)
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
header: "" # HTTP header to look for JWT token (e.g. X-JWT-Assertion). Default is "X-JWT-Assertion"
secret: "" # secret: shared secret key for verifying JWT token signatures (required)
secret: "" # Shared secret key/bytes for verifying JWT token signatures (required, eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)
algorithm: "" # JWT signing algorithm (HS256, HS384, HS512, RS256, ES256). Default is "HS256"
key: "" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: "admin" # secret: the username of the admin user. If not set, the default is "admin".
+20 -4
View File
@@ -83,13 +83,19 @@ export async function generateOTP(username, password) {
let apiPath = getApiPath('auth/otp/generate', params)
const res = await fetch(apiPath, {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-Password': encodeURIComponent(password),
}
})
return await res.json()
const body = await res.json().catch(() => ({}))
if (!res.ok) {
const msg = body.message || 'Failed to generate OTP'
throw new Error(msg)
}
return body
} catch (error) {
notify.showError(error || 'Failed to generate OTP')
notify.showError(error.message || error || 'Failed to generate OTP')
throw error
}
}
@@ -101,16 +107,26 @@ export async function verifyOTP(username, password, otp) {
let apiPath = getApiPath('auth/otp/verify', params)
const res = await fetch(apiPath, {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-Password': encodeURIComponent(password),
'X-Secret': otp,
}
})
if (res.status != 200) {
throw new Error('Failed to verify OTP')
let msg = 'Failed to verify OTP'
try {
const errBody = await res.json()
if (errBody.message) {
msg = errBody.message
}
} catch {
// keep default message
}
throw new Error(msg)
}
} catch (error) {
notify.showError(error || 'Failed to verify OTP')
notify.showError(error.message || error || 'Failed to verify OTP')
throw error
}
}
+2 -2
View File
@@ -86,9 +86,9 @@ export async function fileWatcherLatencyCheck() {
}
// GET /api/tools/fileWatcher
export async function fileWatcher(source, path) {
export async function fileWatcher(source, path, lines) {
try {
const params = { source, path };
const params = { source, path, lines: lines };
const apiPath = getApiPath("tools/fileWatcher", params);
const res = await fetchURL(apiPath);
return await res.json();
+59 -10
View File
@@ -1,6 +1,9 @@
import { fetchURL, fetchJSON } from '@/api/utils'
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
import { notify } from '@/notify'
import { state } from '@/store/state.js'
import { mutations } from '@/store/mutations.js'
import i18n from '@/i18n'
// GET /api/users (list all)
export async function getAllUsers() {
@@ -48,14 +51,18 @@ export async function create(user) {
}
// PUT /api/users (update user)
// Optional `options.headers` (e.g. X-Password when changing a password user's password).
// Password-login: tries without X-Password first; on 401 requiring X-Password, opens the prompt and retries.
// options.skipActorPasswordConfirm / pre-set X-Password skip that flow.
// options.actorPasswordPromptI18nKey — optional vue-i18n key (default: confirmPasswordToSaveUser).
export async function update(user, which = ['all'], options = {}) {
const excludeKeys = ['id', 'name']
which = which.filter(item => !excludeKeys.includes(item))
if (user.username === 'anonymous') {
return
}
const mergedHeaders = { ...(options.headers || {}) }
let userData = user
if (which.length !== 1 || which[0] !== 'all') {
userData = {}
@@ -67,15 +74,57 @@ export async function update(user, which = ['all'], options = {}) {
}
const apiPath = getApiPath('users', { id: user.id })
const extraHeaders = options.headers || {}
await fetchURL(apiPath, {
method: 'PUT',
body: JSON.stringify({
which: which,
data: userData
}),
headers: extraHeaders,
const body = JSON.stringify({
which: which,
data: userData
})
const needsActorPasswordRetry = (err) =>
state.user?.loginMethod === 'password' &&
options.skipActorPasswordConfirm !== true &&
mergedHeaders['X-Password'] === undefined &&
err &&
err.status === 401 &&
typeof err.message === 'string' &&
err.message.includes('X-Password')
try {
await fetchURL(apiPath, {
method: 'PUT',
body,
headers: mergedHeaders,
})
} catch (e) {
if (!needsActorPasswordRetry(e)) {
throw e
}
const promptKey =
options.actorPasswordPromptI18nKey || 'prompts.confirmPasswordToSaveUser'
return new Promise((resolve, reject) => {
mutations.showPrompt({
name: 'password',
props: {
infoText: i18n.global.t(promptKey),
submitLabel: i18n.global.t('general.confirm'),
submitCallback: async (actorPassword) => {
try {
await update(user, which, {
...options,
headers: {
...mergedHeaders,
'X-Password': encodeURIComponent(actorPassword),
},
skipActorPasswordConfirm: true,
})
resolve(undefined)
} catch (err) {
reject(err)
}
},
},
})
})
}
}
// DELETE /api/users (remove user)
+2 -27
View File
@@ -186,6 +186,7 @@ import buttons from "@/utils/buttons";
import { notify } from "@/notify";
import { resourcesApi } from "@/api";
import { url } from "@/utils";
import { copyToClipboard } from "@/utils/clipboard";
function isArchivePath(pathOrName) {
if (!pathOrName || typeof pathOrName !== "string") return false;
@@ -662,33 +663,7 @@ export default {
const item = this.firstSelected;
const path = item?.path || "";
if (!path) {
notify.showErrorToast(this.$t("prompts.copyToClipboardFailed"));
mutations.closeHovers();
return;
}
try {
await navigator.clipboard.writeText(path);
notify.showSuccessToast(this.$t("buttons.copySuccess"));
} catch (err) {
const textArea = document.createElement("textarea");
textArea.value = path;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
notify.showSuccessToast(this.$t("buttons.copySuccess"));
} catch (e) {
notify.showErrorToast(this.$t("prompts.copyToClipboardFailed"));
}
document.body.removeChild(textArea);
}
await copyToClipboard(path);
mutations.closeHovers();
},
showNewDirPrompt() {
+21 -1
View File
@@ -207,7 +207,7 @@ export default {
// Add keyboard event listener for "/" to activate search
this.handleKeydown = (event) => {
if (event.key === '/' && !state.isSearchActive && getters.currentPrompt() === null) {
if (event.key === '/' || event.key === ' ' && !state.isSearchActive && getters.currentPrompt() === null) {
event.preventDefault();
this.open();
}
@@ -546,6 +546,26 @@ export default {
return "";
}
},
open() {
if (state.isSearchActive) return;
if (getters.currentPromptName()) return;
mutations.closeHovers();
mutations.closeSidebar();
mutations.resetSelected();
mutations.setSearch(true);
this.$nextTick(() => {
const input = this.$refs.input;
if (input) {
input.focus();
}
const resultList = document.getElementById("result-list");
if (resultList) {
resultList.classList.add("active");
}
});
},
addSelected(event, s) {
event.preventDefault();
const pathParts = url.removeTrailingSlash(s.path).split("/");
@@ -223,7 +223,8 @@ export default {
fromSource: selectedItem.source,
to: url.joinPath(targetPath, selectedItem.name),
toSource: source,
itemType: selectedItem.type
itemType: selectedItem.type,
name: selectedItem.name
});
}
@@ -554,13 +554,11 @@ export default {
let items = [];
for (let i of state.selected) {
items.push({
// @ts-ignore
from: state.req.items[i].path,
// @ts-ignore
fromSource: state.req.items[i].source,
// @ts-ignore
to: url.joinPath(this.path, state.req.items[i].name),
toSource: this.source,
name: state.req.items[i].name,
});
}
@@ -3,8 +3,8 @@
<!-- API Token Section -->
<div class="api-section">
<button
class="action copy-clipboard api-key-button"
:data-clipboard-text="name"
class="action api-key-button"
@click.stop="copyToClipboard(name)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
>
@@ -12,8 +12,8 @@
<i class="material-symbols">content_paste</i>
</button>
<button
class="action copy-clipboard api-key-value-button"
:data-clipboard-text="info.token"
class="action api-key-value-button"
@click.stop="copyToClipboard(info.token)"
:aria-label="$t('api.clickToCopyKey')"
:title="$t('api.clickToCopyKey')"
>
@@ -73,6 +73,7 @@ import { mutations } from "@/store";
import { notify } from "@/notify";
import { authApi } from "@/api";
import { eventBus } from "@/store/eventBus";
import { copyToClipboard } from "@/utils/clipboard";
export default {
name: "ActionApi",
@@ -93,6 +94,9 @@ export default {
},
},
methods: {
async copyToClipboard(text) {
await copyToClipboard(text);
},
formatTime(timestamp) {
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
year: "numeric",
@@ -81,9 +81,12 @@ export default {
closeTopPrompt() {
mutations.closeTopPrompt();
},
confirm() {
this.onConfirm();
mutations.closeTopPrompt();
async confirm() {
try {
await this.onConfirm();
} finally {
mutations.closeTopPrompt();
}
},
},
};
+2 -20
View File
@@ -86,6 +86,7 @@
<script>
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { formatTimestamp } from "@/utils/moment";
import { copyToClipboard } from "@/utils/clipboard";
import { resourcesApi } from "@/api";
import { state } from "@/store";
import { notify } from "@/notify";
@@ -180,26 +181,7 @@ export default {
},
async copyToClipboard() {
if (!this.hashResult) return;
try {
await navigator.clipboard.writeText(this.hashResult);
notify.showSuccessToast(this.$t("prompts.hashCopied"));
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = this.hashResult;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
notify.showSuccessToast(this.$t("prompts.hashCopied"));
} catch (e) {
notify.showError(this.$t("prompts.errorCopyingHash"));
}
document.body.removeChild(textArea);
}
await copyToClipboard(this.hashResult);
},
},
};
+14 -2
View File
@@ -78,6 +78,16 @@ export default {
source: state.req?.source || null,
};
},
currentPromptName() {
return getters.currentPromptName();
},
},
watch: {
currentPromptName(newName, oldName) {
if (this.creating && oldName === "replace-rename" && newName !== "new-dir") {
this.creating = false;
}
},
},
methods: {
closeTopPrompt() {
@@ -151,13 +161,15 @@ export default {
if (getters.isShare()) {
await resourcesApi.postPublic(state.shareInfo?.hash, newPath, "", false, undefined, {}, true);
mutations.setReload(true);
mutations.closeTopPrompt();
mutations.closeTopPrompt(); // close conflict prompt
mutations.closeTopPrompt(); // close new dir prompt
success = true;
return;
}
await resourcesApi.post(source, newPath, "", false, undefined, {}, true);
mutations.setReload(true);
mutations.closeTopPrompt();
mutations.closeTopPrompt(); // close conflict prompt
mutations.closeTopPrompt(); // close new dir prompt
success = true;
// Show success notification with "go to item" button
+14 -2
View File
@@ -78,6 +78,16 @@ export default {
source: state.req?.source || null,
};
},
currentPromptName() {
return getters.currentPromptName();
},
},
watch: {
currentPromptName(newName, oldName) {
if (this.creating && oldName === "replace-rename" && newName !== "new-file") {
this.creating = false;
}
},
},
methods: {
async submit(event) {
@@ -148,13 +158,15 @@ export default {
if (getters.isShare()) {
await resourcesApi.postPublic(state.shareInfo?.hash, newPath, "", false);
mutations.setReload(true);
mutations.closeTopPrompt();
mutations.closeTopPrompt(); // close conflict prompt
mutations.closeTopPrompt(); // close new file prompt
success = true;
return;
}
await resourcesApi.post(source, newPath, "", false);
mutations.setReload(true);
mutations.closeTopPrompt();
mutations.closeTopPrompt(); // close conflict prompt
mutations.closeTopPrompt(); // close new file prompt
success = true;
// Show success notification with "go to item" button
@@ -287,6 +287,8 @@ export default {
// convert to lowercase
// Explicit switch statement for compile-time safety with ESLint i18n validation
switch (promptName.toLowerCase()) {
case "user-edit":
return this.$t("settings.modifyOtherUser");
case "delete":
return this.$t("prompts.deleteTitle");
case "access":
+6 -37
View File
@@ -57,14 +57,14 @@
</button>
</td>
<td class="small">
<button class="action copy-clipboard" :data-clipboard-text="link.shareURL"
<button class="action" @click.stop="copyToClipboard(link.shareURL)"
:aria-label="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')">
<i class="material-symbols">content_paste</i>
</button>
</td>
<td class="small">
<button :disabled="link.shareType == 'upload'" class="action copy-clipboard"
:data-clipboard-text="link.downloadURL" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
<button :disabled="link.shareType == 'upload'" class="action" @click.stop="copyToClipboard(link.downloadURL)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')">
<i class="material-symbols">content_paste_go</i>
</button>
@@ -321,7 +321,7 @@
import { notify } from "@/notify";
import { state, getters, mutations } from "@/store";
import { shareApi } from "@/api";
import Clipboard from "clipboard";
import { copyToClipboard } from "@/utils/clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl } from "@/utils/url";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
@@ -364,8 +364,6 @@ export default {
unit: "hours",
/** @type {Share[]} */
links: [],
/** @type {Clipboard | null} */
clip: null,
password: "",
listing: true,
allowModify: false,
@@ -558,10 +556,6 @@ export default {
}
},
mounted() {
// Initialize clipboard after DOM is ready
this.$nextTick(() => {
this.initClipboard();
});
// Listen for sidebar links updates from the SidebarLinks prompt
eventBus.on('shareSidebarLinksUpdated', this.handleSidebarLinksUpdate);
eventBus.on('pathSelected', this.onBannerFaviconPathSelected);
@@ -573,23 +567,10 @@ export default {
eventBus.off('shareSidebarLinksUpdated', this.handleSidebarLinksUpdate);
eventBus.off('pathSelected', this.onBannerFaviconPathSelected);
eventBus.off('pathPickerCancelled', this.onBannerFaviconPathPickerCancelled);
// Clean up clipboard
if (this.clip) {
this.clip.destroy();
}
},
methods: {
initClipboard() {
// Destroy existing clipboard first
if (this.clip) {
this.clip.destroy();
}
// Create new clipboard instance
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
notify.showSuccessToast(this.$t("success.linkCopied"));
});
async copyToClipboard(text) {
await copyToClipboard(text);
},
/**
* @param {MouseEvent} event
@@ -676,10 +657,6 @@ export default {
if (!this.isEditMode && !this.editingLink) {
this.links.push(res);
this.sort();
// reinitialize the clipboard after adding a new link
this.$nextTick(() => {
this.initClipboard();
});
} else if (this.editingLink) {
// Update the link in the local list
const index = this.links.findIndex(l => l.hash === this.editingLink.hash);
@@ -689,10 +666,6 @@ export default {
this.editingLink = null;
// emit event to reload shares in settings view
eventBus.emit('sharesChanged');
// Reinitialize clipboard after edit the share
this.$nextTick(() => {
this.initClipboard();
});
} else {
// emit event to reload shares in settings view
eventBus.emit('sharesChanged');
@@ -767,10 +740,6 @@ export default {
try {
await shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
// Reinitialize clipboard after deletion too
this.$nextTick(() => {
this.initClipboard();
});
// emit event to reload shares in settings view
eventBus.emit('sharesChanged');
if (this.links.length === 0) {
@@ -59,11 +59,9 @@
<!-- Link Type Selection -->
<p>{{ $t('sidebar.linkType') }}</p>
<select aria-label="Link Type" v-model="newLink.category" @change="handleCategoryChange" class="input">
<select aria-label="Link Type" :value="linkTypeSelectValue" @change="onLinkTypeChange" class="input">
<option value="">{{ $t('sidebar.selectLinkType') }}</option>
<option v-if="context === 'user'" value="source">{{ $t('general.source') }}</option>
<option v-if="context === 'user'" value="source-minimal">{{ $t('general.source') }}</option>
<option v-if="context === 'user'" value="source-hybrid">{{ $t('general.source') }}</option>
<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>
@@ -342,6 +340,13 @@ export default {
}
return 'indexed';
},
/** Single "Source" row in the link-type dropdown; actual variant stays in newLink.category (toggles). */
linkTypeSelectValue() {
if (this.isSourceCategory(this.newLink.category)) {
return 'source';
}
return this.newLink.category;
},
},
async mounted() {
// Initialize with existing sidebar links based on context
@@ -496,21 +501,32 @@ export default {
}
return link.name;
},
handleCategoryChange() {
// Reset newLink fields when category changes
resetNewLinkFormForTypeSwitch() {
this.newLink.name = "";
this.newLink.target = "";
this.newLink.icon = "";
this.newLink.sourceName = "";
this.newLink.sourcePath = "";
// Set default name and icon for special share link types
if (this.newLink.category === "shareInfo") {
this.newLink.name = this.$t("share.shareInfo");
this.newLink.icon = "qr_code";
} else if (this.newLink.category === "download") {
this.newLink.name = this.$t("general.download");
this.newLink.icon = "download";
},
onLinkTypeChange(event) {
const value = event.target.value;
if (value === "source") {
if (!this.isSourceCategory(this.newLink.category)) {
this.resetNewLinkFormForTypeSwitch();
this.newLink.category = "source";
}
return;
}
if (value !== this.newLink.category || this.isSourceCategory(this.newLink.category)) {
this.resetNewLinkFormForTypeSwitch();
this.newLink.category = value;
if (value === "shareInfo") {
this.newLink.name = this.$t("share.shareInfo");
this.newLink.icon = "qr_code";
} else if (value === "download") {
this.newLink.name = this.$t("general.download");
this.newLink.icon = "download";
}
}
},
handleSourceChange() {
+27 -6
View File
@@ -7,12 +7,13 @@
<qrcode-vue class="qrcode" :value="url" :size="200" level="M"></qrcode-vue>
</div>
<p>{{ $t("otp.verifyInstructions") }}</p>
<input :class="{'form-invalid': error != '' && code != ''}" v-focus class="input" type="text" v-model="code" @keyup.enter="verifyCode"
<input :class="{'form-invalid': error != '' && code != ''}" v-focus class="input" type="text" v-model="code"
@keydown.enter.prevent="verifyCode"
:placeholder="$t('otp.codeInputPlaceholder')" />
</div>
<div class="card-actions">
<button v-if="!succeeded && code != ''" class="button button--flat button--blue" @click="verifyCode"
<button v-if="!succeeded && code != ''" type="button" class="button button--flat button--blue" @click="verifyCode"
:title="$t('general.verify')">
{{ $t("general.verify") }}
</button>
@@ -37,6 +38,7 @@ export default {
code: "",
url: "",
succeeded: false,
verifyInFlight: false,
};
},
props: {
@@ -59,6 +61,10 @@ export default {
},
async mounted() {
if (this.generate) {
if (String(this.password ?? "").trim() === "") {
this.error = this.$t("otp.passwordRequiredForGeneration");
return;
}
this.generateNewCode();
}
},
@@ -69,6 +75,10 @@ export default {
},
methods: {
async generateNewCode() {
if (String(this.password ?? "").trim() === "") {
this.error = this.$t("otp.passwordRequiredForGeneration");
return;
}
try {
const resp = await authApi.generateOTP(this.username, this.password);
this.url = resp.url;
@@ -78,28 +88,39 @@ export default {
}
},
async verifyCode(event) {
event.preventDefault();
event?.preventDefault?.();
if (this.verifyInFlight || this.succeeded) {
return;
}
if (!this.code) {
this.error = this.$t("otp.invalidCodeType");
notify.showError(this.$t("otp.invalidCodeType"));
return;
}
if (String(this.password ?? "").trim() === "") {
this.error = this.$t("otp.passwordRequiredForGeneration");
notify.showError(this.$t("otp.passwordRequiredForGeneration"));
return;
}
this.verifyInFlight = true;
try {
await authApi.verifyOTP(this.username, this.password, this.code);
if (this.redirect != "") {
await authApi.login(this.username, this.password, this.redirect, this.code);
await initAuth();
this.$router.push(this.redirect);
// Wait for navigation so Layout mounts and registers the toast callback before we toast
await this.$router.push(this.redirect);
}
mutations.updateCurrentUser({ otpEnabled: true });
this.succeeded = true
this.succeeded = true;
this.error = "";
notify.showSuccessToast(this.$t("otp.verificationSucceed"));
mutations.closeTopPrompt();
} catch (error) {
this.error = this.$t("otp.verificationFailed");
console.log("error", error);
return;
} finally {
this.verifyInFlight = false;
}
},
},
+24 -26
View File
@@ -434,11 +434,20 @@ export default {
},
newOTP() {
mutations.showPrompt({
name: "totp",
name: "password",
props: {
generate: true,
username: this.user.username,
password: this.passwordRef || this.user.password || "",
infoText: this.$t("prompts.confirmPasswordToSaveUser"),
submitLabel: this.$t("general.confirm"),
submitCallback: (accountPassword) => {
mutations.showPrompt({
name: "totp",
props: {
generate: true,
username: this.user.username,
password: accountPassword,
},
});
},
},
});
},
@@ -447,28 +456,17 @@ export default {
if (!this.canUpdatePassword) {
return;
}
mutations.showPrompt({
name: "password",
props: {
infoText: this.$t("prompts.confirmPasswordToChangeUserPassword"),
submitLabel: this.$t("general.confirm"),
submitCallback: async (actorPassword) => {
try {
await usersApi.update(this.user, ["password"], {
headers: {
"X-Password": encodeURIComponent(actorPassword),
},
});
if (state.user.permissions.admin && this.user.id !== state.user.id) {
eventBus.emit("usersChanged");
}
notify.showSuccessToast(this.$t("settings.userUpdated"));
} catch (e) {
notify.showError(e);
}
},
},
});
try {
await usersApi.update(this.user, ["password"], {
actorPasswordPromptI18nKey: "prompts.confirmPasswordToSaveUser",
});
if (state.user.permissions.admin && this.user.id !== state.user.id) {
eventBus.emit("usersChanged");
}
notify.showSuccessToast(this.$t("settings.userUpdated"));
} catch (e) {
notify.showError(e);
}
},
emitUserUpdate() {
// Update the user object with current scopes
+2 -3
View File
@@ -39,7 +39,7 @@
</div>
</a>
<!-- Source-type links (source, source-minimal, source-alt, source-hybrid, source-hybrid-2); usage bar hidden for source-minimal -->
<a v-else-if="link.category === 'source' || link.category === 'source-minimal' || link.category === 'source-alt' || link.category === 'source-hybrid' || link.category === 'source-hybrid-2'" :href="getLinkHref(link)"
<a v-else-if="isSourceCategory(link.category)" :href="getLinkHref(link)"
class="action button source-button sidebar-link-button" :class="{
active: isLinkActive(link),
disabled: !isLinkAccessible(link)
@@ -318,8 +318,7 @@ export default {
},
methods: {
isSourceCategory(category) {
return category === 'source' || category === 'source-minimal' || category === 'source-alt' ||
category === 'source-hybrid' || category === 'source-hybrid-2';
return category.startsWith("source")
},
getIconClass,
hasUsageInfo(link) {
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "تم النسخ إلى الحافظة!",
"copyFailed": "فشل النسخ إلى الحافظة",
"copyFile": "نسخ الملف",
"copyToClipboard": "نسخ الى الحافظة",
"moveFile": "نقل الملف",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "تحديد خوارزمية تجزئة",
"generate": "توليد",
"errorGeneratingHash": "خطأ في توليد التجزئة",
"hashCopied": "التجزئة المنسوخة إلى الحافظة",
"errorCopyingHash": "خطأ في نسخ تجزئة التجزئة",
"token": "الرمز المميز",
"onlyOfficeId": "معرّف المكتب فقط",
"firstLoadTitle": "مرحبًا بك في متصفح الملفات الكمي!",
@@ -227,7 +226,6 @@
"renameSuccess": "تمت إعادة تسمية العنصر بنجاح!",
"newFileSuccess": "تم إنشاء الملف بنجاح!",
"newDirSuccess": "تم إنشاء الدليل بنجاح!",
"copyToClipboardFailed": "تعذر النسخ إلى الحافظة",
"moveFailed": "فشلت عملية النقل",
"operationFailed": "فشلت العملية",
"partialSuccess": "فشلت بعض العمليات: {succeeded} نجحت ، {failed} فشلت ",
@@ -243,7 +241,7 @@
"unarchiveDestination": "مقتطف إلى:",
"unarchiveSuccess": "تم استخراج الأرشيف بنجاح!",
"downloadFailed": "فشل التحميل المقطوع - إذا تكرر ذلك، حاول تقليل حجم القطعة: {message}",
"confirmPasswordToChangeUserPassword": "أدخل كلمة المرور الحالية للسماح بتغيير كلمة المرور هذه."
"confirmPasswordToSaveUser": "أدخل كلمة المرور الحالية لحفظ تغييرات المستخدم هذه."
},
"search": {
"smallerThan": "أصغر من:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "تم تمكين OTP بنجاح",
"verificationFailed": "فشل التحقق",
"verificationSucceed": "نجاح التحقق",
"generationFailed": "فشل التوليد"
"generationFailed": "فشل التوليد",
"passwordRequiredForGeneration": "كلمة المرور الخاصة بك مطلوبة لإنشاء رمز المصادقة."
},
"general": {
"name": "الاسم",
@@ -845,8 +844,6 @@
"showingResults": "عرض {count} النتائج ",
"noResults": "لم يتم العثور على أي أيقونات",
"tryDifferentSearch": "جرّب مصطلح بحث مختلف",
"copiedToClipboard": "نسخ \"{name}\" إلى الحافظة!",
"copyFailed": "فشل النسخ إلى الحافظة",
"custom": "مخصص",
"useCustomIcon": "استخدام أيقونة مخصصة: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Zkopírováno do schránky!",
"copyFailed": "Kopírování do schránky se nezdařilo",
"generateNewOtp": "Resetovat a vygenerovat nový kód dvoufázového ověření",
"copyFile": "Kopírovat soubor",
"copyToClipboard": "Kopírovat do schránky",
@@ -293,8 +294,6 @@
"selectHashAlgorithm": "Výběr algoritmu hash",
"generate": "Generovat",
"errorGeneratingHash": "Chyba při generování hash",
"hashCopied": "Hash zkopírovaný do schránky",
"errorCopyingHash": "Chyba při kopírování hash",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Vítejte v aplikaci FileBrowser Quantum!",
@@ -305,7 +304,6 @@
"renameSuccess": "Položka byla úspěšně přejmenována!",
"newFileSuccess": "Soubor byl úspěšně vytvořen!",
"newDirSuccess": "Adresář byl úspěšně vytvořen!",
"copyToClipboardFailed": "Nelze zkopírovat do schránky",
"moveFailed": "Přesun se nezdařil",
"operationFailed": "Operace se nezdařila",
"partialSuccess": "Některé operace se nezdařily: {succeeded} uspěl, {failed} neuspěl.",
@@ -321,7 +319,7 @@
"unarchiveDestination": "Výpis na:",
"unarchiveSuccess": "Archiv úspěšně extrahován!",
"downloadFailed": "Stahování po částech se nezdařilo - pokud se tato situace opakuje, zkuste zmenšit velikost části: {message}",
"confirmPasswordToChangeUserPassword": "Zadejte své aktuální heslo a autorizujte tuto změnu hesla."
"confirmPasswordToSaveUser": "Pro uložení těchto uživatelských změn zadejte své aktuální heslo."
},
"search": {
"smallerThan": "Menší než:",
@@ -609,7 +607,8 @@
"enabledSuccessfully": "OTP úspěšně aktivováno",
"verificationFailed": "Ověření se nezdařilo",
"verificationSucceed": "Ověření proběhlo úspěšně",
"generationFailed": "Generování selhalo"
"generationFailed": "Generování selhalo",
"passwordRequiredForGeneration": "Pro vygenerování ověřovacího kódu je nutné vaše heslo."
},
"fileLoading": {
"title": "Nahrávání a stahování",
@@ -845,8 +844,6 @@
"showingResults": "Zobrazují se výsledky {count} ",
"noResults": "Nebyly nalezeny žádné ikony",
"tryDifferentSearch": "Zkuste použít jiný vyhledávací výraz",
"copiedToClipboard": "Zkopírováno '{name}' do schránky!",
"copyFailed": "Nepodařilo se zkopírovat do schránky",
"custom": "Vlastní",
"useCustomIcon": "Použití vlastní ikony: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "In die Zwischenablage kopiert!",
"copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen",
"copyFile": "Datei kopieren",
"copyToClipboard": "In Zwischenablage kopieren",
"moveFile": "Datei verschieben",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Wählen Sie einen Hash-Algorithmus",
"generate": "Erzeugen Sie",
"errorGeneratingHash": "Fehler bei der Hash-Erzeugung",
"hashCopied": "Hash in Zwischenablage kopiert",
"errorCopyingHash": "Fehler beim Kopieren des Hashes",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Willkommen bei FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Artikel erfolgreich umbenannt!",
"newFileSuccess": "Datei erfolgreich erstellt!",
"newDirSuccess": "Verzeichnis erfolgreich erstellt!",
"copyToClipboardFailed": "Kopieren in die Zwischenablage nicht möglich",
"moveFailed": "Verschieben fehlgeschlagen",
"operationFailed": "Operation fehlgeschlagen",
"partialSuccess": "Einige Operationen sind fehlgeschlagen: {succeeded} erfolgreich, {failed} fehlgeschlagen",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Auszug zu:",
"unarchiveSuccess": "Archiv erfolgreich entpackt!",
"downloadFailed": "Chunked Download fehlgeschlagen - falls sich dies wiederholt, versuchen Sie, die Chunk-Größe zu verringern: {message}",
"confirmPasswordToChangeUserPassword": "Geben Sie Ihr aktuelles Passwort ein, um diese Passwortänderung zu autorisieren."
"confirmPasswordToSaveUser": "Geben Sie Ihr aktuelles Kennwort ein, um die Benutzeränderungen zu speichern."
},
"search": {
"smallerThan": "Kleiner als:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP erfolgreich aktiviert",
"verificationFailed": "Verifizierung fehlgeschlagen",
"verificationSucceed": "Überprüfung Erfolgreich",
"generationFailed": "Generation gescheitert"
"generationFailed": "Generation gescheitert",
"passwordRequiredForGeneration": "Ihr Passwort ist erforderlich, um einen Authentifizierungscode zu generieren."
},
"general": {
"name": "Name",
@@ -845,8 +844,6 @@
"showingResults": "{count} Ergebnisse anzeigen",
"noResults": "Keine Icons gefunden",
"tryDifferentSearch": "Versuchen Sie einen anderen Suchbegriff",
"copiedToClipboard": "Kopiert '{name}' in die Zwischenablage!",
"copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen",
"custom": "Benutzerdefiniert",
"useCustomIcon": "Benutzerdefiniertes Symbol verwenden: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Αντιγράφηκε στο πρόχειρο!",
"copyFailed": "Απέτυχε η αντιγραφή στο πρόχειρο",
"copyFile": "Αντιγραφή αρχείου",
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
@@ -218,8 +219,6 @@
"selectHashAlgorithm": "Επιλέξτε έναν αλγόριθμο κατακερματισμού",
"generate": "Δημιουργία",
"errorGeneratingHash": "Σφάλμα δημιουργίας hash",
"hashCopied": "Αντιγραφή κατακερματισμού στο πρόχειρο",
"errorCopyingHash": "Σφάλμα αντιγραφής hash",
"token": "Token",
"onlyOfficeId": "ΜόνοΤαυτότητα γραφείου",
"firstLoadTitle": "Καλώς ήρθατε στο FileBrowser Quantum!",
@@ -230,7 +229,6 @@
"renameSuccess": "Το στοιχείο μετονομάστηκε επιτυχώς!",
"newFileSuccess": "Το αρχείο δημιουργήθηκε με επιτυχία!",
"newDirSuccess": "Ο κατάλογος δημιουργήθηκε με επιτυχία!",
"copyToClipboardFailed": "Αδυναμία αντιγραφής στο πρόχειρο",
"moveFailed": "Η μετακίνηση απέτυχε",
"operationFailed": "Η λειτουργία απέτυχε",
"partialSuccess": "Ορισμένες λειτουργίες απέτυχαν: {succeeded} πέτυχε, {failed} απέτυχε",
@@ -246,7 +244,7 @@
"unarchiveDestination": "Απόσπασμα προς:",
"unarchiveSuccess": "Το αρχείο εξήχθη με επιτυχία!",
"downloadFailed": "Αποτυχημένη λήψη σε τεμάχια - αν αυτό επαναληφθεί, δοκιμάστε να μειώσετε το μέγεθος των τεμαχίων: {message}",
"confirmPasswordToChangeUserPassword": "Εισάγετε τον τρέχοντα κωδικό πρόσβασής σας για να εγκρίνετε αυτή την αλλαγή κωδικού πρόσβασης."
"confirmPasswordToSaveUser": "Εισάγετε τον τρέχοντα κωδικό πρόσβασης για να αποθηκεύσετε αυτές τις αλλαγές χρήστη."
},
"search": {
"smallerThan": "Μικρότερο από:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "Το OTP ενεργοποιήθηκε με επιτυχία",
"verificationFailed": "Η επαλήθευση απέτυχε",
"verificationSucceed": "Επαλήθευση Succeed",
"generationFailed": "Γενιά που απέτυχε"
"generationFailed": "Γενιά που απέτυχε",
"passwordRequiredForGeneration": "Ο κωδικός πρόσβασής σας απαιτείται για τη δημιουργία ενός κωδικού ελέγχου ταυτότητας."
},
"general": {
"name": "Όνομα",
@@ -845,8 +844,6 @@
"showingResults": "Εμφάνιση {count} αποτελεσμάτων",
"noResults": "Δεν βρέθηκαν εικονίδια",
"tryDifferentSearch": "Δοκιμάστε έναν διαφορετικό όρο αναζήτησης",
"copiedToClipboard": "Αντιγραφή του '{name}' στο πρόχειρο!",
"copyFailed": "Απέτυχε η αντιγραφή στο πρόχειρο",
"custom": "Προσαρμοσμένο",
"useCustomIcon": "Χρήση προσαρμοσμένου εικονιδίου: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -132,6 +132,7 @@
},
"buttons": {
"copySuccess": "Copied to clipboard!",
"copyFailed": "Failed to copy to clipboard",
"generateNewOtp": "Reset and generate new two-factor code",
"copyFile": "Copy file",
"copyPath": "Copy Path",
@@ -440,9 +441,6 @@
"selectHashAlgorithm": "Select a hash algorithm",
"generate": "Generate",
"errorGeneratingHash": "Error generating hash",
"hashCopied": "Hash copied to clipboard",
"copyToClipboardFailed": "Unable to copy to clipboard",
"errorCopyingHash": "Error copying hash",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Welcome to FileBrowser Quantum!",
@@ -452,7 +450,7 @@
"moveFailed": "Move failed",
"operationFailed": "Operation failed",
"partialSuccess": "Some operations failed: {succeeded} succeeded, {failed} failed",
"confirmPasswordToChangeUserPassword": "Enter your current password to authorize this password change."
"confirmPasswordToSaveUser": "Enter your current password to save these user changes."
},
"search": {
"searchContext": "Search Context: {context}",
@@ -740,7 +738,8 @@
"enabledSuccessfully": "OTP enabled successfully",
"verificationFailed": "Verification Failed",
"verificationSucceed": "Verification Succeed",
"generationFailed": "Generation Failed"
"generationFailed": "Generation Failed",
"passwordRequiredForGeneration": "Your password is required to generate an authenticator code."
},
"access": {
"all": "Deny All",
@@ -855,8 +854,6 @@
"showingResults": "Showing {count} results",
"noResults": "No icons found",
"tryDifferentSearch": "Try a different search term",
"copiedToClipboard": "Copied '{name}' to clipboard!",
"copyFailed": "Failed to copy to clipboard",
"custom": "Custom",
"useCustomIcon": "Use custom icon: {name}"
},
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "¡Copiado al portapapeles!",
"copyFailed": "Error al copiar al portapapeles",
"copyFile": "Copiar archivo",
"copyToClipboard": "Copiar al portapapeles",
"moveFile": "Mover archivo",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Selecciona un algoritmo",
"generate": "Generar",
"errorGeneratingHash": "Hubo un error al obtener el hash",
"hashCopied": "¡Se copio el hash al portapapeles!",
"errorCopyingHash": "Hubo un error al copiar el hash",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "¡Bienvenido a FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "El cambio de nombre se ha realizado correctamente",
"newFileSuccess": "¡Archivo creado con éxito!",
"newDirSuccess": "¡Carpeta creada con éxito!",
"copyToClipboardFailed": "Hubo un error al copiar en el portapapeles",
"moveFailed": "Error al mover",
"operationFailed": "Operación fallida",
"partialSuccess": "Algunas acciones fallaron: {succeeded} se completaron, y {failed} fallaron",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extraer a:",
"unarchiveSuccess": "¡Archivo extraído con éxito!",
"downloadFailed": "Fallo en la descarga por trozos. Si se repite, intente reducir el tamaño de los trozos: {message}",
"confirmPasswordToChangeUserPassword": "Introduzca su contraseña actual para autorizar este cambio de contraseña."
"confirmPasswordToSaveUser": "Introduzca su contraseña actual para guardar estos cambios de usuario."
},
"search": {
"smallerThan": "Menor que:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "¡2FA activado!",
"verificationFailed": "No se pudo autenticar",
"verificationSucceed": "¡Verificación exitosa!",
"generationFailed": "Hubo un error al generar el código"
"generationFailed": "Hubo un error al generar el código",
"passwordRequiredForGeneration": "Su contraseña es necesaria para generar un código autenticador."
},
"general": {
"name": "Nombre",
@@ -845,8 +844,6 @@
"showingResults": "Se encontraron {count} iconos",
"noResults": "No se pudieorn encontrar iconos",
"tryDifferentSearch": "Prueba con otro nombre",
"copiedToClipboard": "¡Se copió '{name}' al portapapeles!",
"copyFailed": "Hubo un error al copiar al portapapeles",
"custom": "Personalizar",
"useCustomIcon": "Utilizar icono personalizado: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Copié dans le presse-papiers !",
"copyFailed": "Échec de la copie dans le presse-papiers",
"copyFile": "Copier le fichier",
"copyToClipboard": "Copier dans le presse-papier",
"moveFile": "Déplacer le fichier",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Sélectionner un algorithme de hachage",
"generate": "Générer",
"errorGeneratingHash": "Erreur dans la génération du hachage",
"hashCopied": "Hash copié dans le presse-papiers",
"errorCopyingHash": "Erreur de copie du hachage",
"token": "Jeton",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Bienvenue à FileBrowser Quantum !",
@@ -227,7 +226,6 @@
"renameSuccess": "L'élément a été renommé avec succès !",
"newFileSuccess": "Fichier créé avec succès !",
"newDirSuccess": "Répertoire créé avec succès !",
"copyToClipboardFailed": "Impossible de copier dans le presse-papiers",
"moveFailed": "Échec du déplacement",
"operationFailed": "Échec de l'opération",
"partialSuccess": "Certaines opérations ont échoué : {succeeded} a réussi, {failed} a échoué",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extrait à :",
"unarchiveSuccess": "L'archive a été extraite avec succès !",
"downloadFailed": "Échec du téléchargement groupé - si cela se répète, essayez de réduire la taille des morceaux : {message}",
"confirmPasswordToChangeUserPassword": "Saisissez votre mot de passe actuel pour autoriser ce changement de mot de passe."
"confirmPasswordToSaveUser": "Saisissez votre mot de passe actuel pour enregistrer ces changements d'utilisateur."
},
"search": {
"smallerThan": "Plus petit que :",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP activé avec succès",
"verificationFailed": "Échec de la vérification",
"verificationSucceed": "Vérification réussie",
"generationFailed": "Génération échouée"
"generationFailed": "Génération échouée",
"passwordRequiredForGeneration": "Votre mot de passe est nécessaire pour générer un code d'authentification."
},
"general": {
"name": "Nom",
@@ -845,8 +844,6 @@
"showingResults": "Afficher les résultats de {count} ",
"noResults": "Aucune icône trouvée",
"tryDifferentSearch": "Essayez un autre terme de recherche",
"copiedToClipboard": "Copie de '{name}' dans le presse-papiers !",
"copyFailed": "Échec de la copie dans le presse-papiers",
"custom": "Sur mesure",
"useCustomIcon": "Utiliser une icône personnalisée : {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "הועתק ללוח!",
"copyFailed": "העתקה ללוח נכשלה",
"copyFile": "העתק קובץ",
"copyToClipboard": "העתק ללוח",
"moveFile": "העבר קובץ",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "בחר אלגוריתם hash",
"generate": "ליצור",
"errorGeneratingHash": "שגיאה ביצירת hash",
"hashCopied": "ה-hash הועתק ללוח",
"errorCopyingHash": "שגיאה בהעתקת hash",
"token": "אסימון",
"onlyOfficeId": "מזהה OnlyOffice",
"firstLoadTitle": "ברוכים הבאים ל-FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "הפריט שונה בהצלחה!",
"newFileSuccess": "הקובץ נוצר בהצלחה!",
"newDirSuccess": "הספרייה נוצרה בהצלחה!",
"copyToClipboardFailed": "לא ניתן להעתיק ללוח",
"moveFailed": "העברה נכשלה",
"operationFailed": "הפעולה נכשלה",
"partialSuccess": "כמה פעולות נכשלו: {succeeded} הצליחו, {failed} נכשלו",
@@ -243,7 +241,7 @@
"unarchiveDestination": "חלץ אל:",
"unarchiveSuccess": "הארכיון חולץ בהצלחה!",
"downloadFailed": "ההורדה המקוטעת נכשלה - אם הבעיה חוזרת על עצמה, נסה להקטין את גודל החלקים: {message}",
"confirmPasswordToChangeUserPassword": "הזן את הסיסמה הנוכחית שלך כדי לאשר את שינוי הסיסמה."
"confirmPasswordToSaveUser": "הזן את הסיסמה הנוכחית שלך כדי לשמור את שינויי המשתמש."
},
"search": {
"smallerThan": "קטן מ:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP הופעל בהצלחה",
"verificationFailed": "האימות נכשל",
"verificationSucceed": "האימות הצליח",
"generationFailed": "דור שנכשל"
"generationFailed": "דור שנכשל",
"passwordRequiredForGeneration": "יש להזין את הסיסמה שלך כדי ליצור קוד אימות."
},
"general": {
"name": "שם",
@@ -845,8 +844,6 @@
"showingResults": "הצגת תוצאות {count} ",
"noResults": "לא נמצאו סמלים",
"tryDifferentSearch": "נסה מונח חיפוש אחר",
"copiedToClipboard": "הועתק '{name}' ללוח!",
"copyFailed": "לא ניתן להעתיק ללוח",
"custom": "מותאם אישית",
"useCustomIcon": "השתמש בסמל מותאם אישית: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Vágólapra másolva!",
"copyFailed": "Nem sikerült a vágólapra másolni",
"copyFile": "Fájl másolása",
"copyToClipboard": "Másolás vágólapra",
"moveFile": "Fájl mozgatása",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Válasszon ki egy hash algoritmust",
"generate": "Generálja",
"errorGeneratingHash": "Hiba generáló hash",
"hashCopied": "Hash másolva a vágólapra",
"errorCopyingHash": "Hiba a hash másolásában",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Üdvözöljük a FileBrowser Quantum-on!",
@@ -227,7 +226,6 @@
"renameSuccess": "Az elem átnevezése sikeres volt!",
"newFileSuccess": "A fájl sikeresen létrehozva!",
"newDirSuccess": "A könyvtárat sikeresen létrehoztuk!",
"copyToClipboardFailed": "Nem lehet a vágólapra másolni",
"moveFailed": "Áthelyezés sikertelen",
"operationFailed": "A művelet sikertelen",
"partialSuccess": "Néhány művelet sikertelen: {succeeded} sikeres volt, {failed} sikertelen.",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Kivonat a következő címre:",
"unarchiveSuccess": "Az archívum sikeresen kinyomtatva!",
"downloadFailed": "A darabos letöltés sikertelen - ha ez ismétlődik, próbálja meg csökkenteni a darabos letöltés méretét: {message}",
"confirmPasswordToChangeUserPassword": "A jelszóváltoztatás engedélyezéséhez adja meg a jelenlegi jelszavát."
"confirmPasswordToSaveUser": "A felhasználói módosítások mentéséhez adja meg az aktuális jelszavát."
},
"search": {
"smallerThan": "Kisebb, mint:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP sikeresen engedélyezve",
"verificationFailed": "Ellenőrzés sikertelen",
"verificationSucceed": "Ellenőrzés Succeed",
"generationFailed": "Nemzedék sikertelen"
"generationFailed": "Nemzedék sikertelen",
"passwordRequiredForGeneration": "A hitelesítési kód generálásához a jelszóra van szükség."
},
"general": {
"name": "Név",
@@ -845,8 +844,6 @@
"showingResults": "{count} eredmények megjelenítése",
"noResults": "Nem talált ikonok",
"tryDifferentSearch": "Próbáljon ki egy másik kereső kifejezést",
"copiedToClipboard": "Másolt '{name}' a vágólapra!",
"copyFailed": "Nem sikerült a vágólapra másolni",
"custom": "Custom",
"useCustomIcon": "Egyéni ikon használata: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -132,6 +132,7 @@
},
"buttons": {
"copySuccess": "Copiato negli appunti!",
"copyFailed": "Copia negli appunti fallita",
"generateNewOtp": "Resetta e genera nuovo codice a due fattori",
"copyFile": "Copia file",
"copyToClipboard": "Copia negli appunti",
@@ -439,9 +440,6 @@
"selectHashAlgorithm": "Seleziona un algoritmo hash",
"generate": "Genera",
"errorGeneratingHash": "Errore durante la generazione dell'hash",
"hashCopied": "Hash copiato negli appunti",
"copyToClipboardFailed": "Impossibile copiare negli appunti",
"errorCopyingHash": "Errore durante la copia dell'hash",
"token": "Token",
"onlyOfficeId": "ID OnlyOffice",
"firstLoadTitle": "Benvenuto in FileBrowser Quantum!",
@@ -452,7 +450,7 @@
"operationFailed": "Operazione fallita",
"partialSuccess": "Alcune operazioni sono fallite: {succeeded} riuscite, {failed} fallite",
"downloadFailed": "Il download in chunk non è riuscito: se il problema si ripete, provare a ridurre la dimensione del chunk: {message}",
"confirmPasswordToChangeUserPassword": "Inserire la password attuale per autorizzare la modifica della password."
"confirmPasswordToSaveUser": "Immettere la password attuale per salvare le modifiche apportate all'utente."
},
"search": {
"searchContext": "Contesto di ricerca: {context}",
@@ -740,7 +738,8 @@
"enabledSuccessfully": "OTP abilitato con successo",
"verificationFailed": "Verifica fallita",
"verificationSucceed": "Verifica riuscita",
"generationFailed": "Generazione fallita"
"generationFailed": "Generazione fallita",
"passwordRequiredForGeneration": "La password è necessaria per generare un codice autenticatore."
},
"access": {
"all": "Nega Tutto",
@@ -855,8 +854,6 @@
"showingResults": "Mostrando {count} risultati",
"noResults": "Nessuna icona trovata",
"tryDifferentSearch": "Prova un termine diverso",
"copiedToClipboard": "Copiato '{name}' negli appunti!",
"copyFailed": "Copia fallita",
"custom": "Personalizzata",
"useCustomIcon": "Usa icona personalizzata: {name}"
},
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "クリップボードにコピーしました!",
"copyFailed": "クリップボードへのコピーに失敗しました",
"copyFile": "ファイルをコピー",
"copyToClipboard": "クリップボードにコピー",
"moveFile": "ファイルを移動",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "ハッシュアルゴリズムの選択",
"generate": "生成する",
"errorGeneratingHash": "ハッシュ生成エラー",
"hashCopied": "クリップボードにコピーされたハッシュ",
"errorCopyingHash": "ハッシュのコピーエラー",
"token": "トークン",
"onlyOfficeId": "オンリーオフィスID",
"firstLoadTitle": "FileBrowser Quantumへようこそ!",
@@ -227,7 +226,6 @@
"renameSuccess": "アイテム名の変更に成功しました!",
"newFileSuccess": "ファイルは正常に作成されました!",
"newDirSuccess": "ディレクトリは正常に作成されました!",
"copyToClipboardFailed": "クリップボードにコピーできない",
"moveFailed": "移動に失敗しました",
"operationFailed": "操作失敗",
"partialSuccess": "{succeeded} は成功、{failed} は失敗。",
@@ -243,7 +241,7 @@
"unarchiveDestination": "に抜粋する:",
"unarchiveSuccess": "アーカイブは正常に抽出されました!",
"downloadFailed": "チャンクダウンロードに失敗しました - これが繰り返される場合は、チャンクサイズを小さくしてみてください:{message}",
"confirmPasswordToChangeUserPassword": "現在のパスワードを入力して、パスワードの変更を承認します。"
"confirmPasswordToSaveUser": "現在のパスワードを入力して、ユーザーの変更を保存します。"
},
"search": {
"smallerThan": "より小さい:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTPの有効化に成功しました",
"verificationFailed": "検証に失敗しました",
"verificationSucceed": "検証に成功しました",
"generationFailed": "生成に失敗しました"
"generationFailed": "生成に失敗しました",
"passwordRequiredForGeneration": "認証コードを生成するにはパスワードが必要です。"
},
"general": {
"name": "名前",
@@ -845,8 +844,6 @@
"showingResults": "{count} の結果を表示",
"noResults": "アイコンが見つかりません",
"tryDifferentSearch": "別の検索語を試す",
"copiedToClipboard": "{name}' をクリップボードにコピー!",
"copyFailed": "クリップボードへのコピーに失敗しました",
"custom": "カスタム",
"useCustomIcon": "カスタムアイコンを使用する:{name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "클립보드에 복사되었습니다!",
"copyFailed": "클립보드 복사 실패",
"copyFile": "파일 복사",
"copyToClipboard": "클립보드 복사",
"moveFile": "파일 이동",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "해시 알고리즘 선택",
"generate": "생성",
"errorGeneratingHash": "해시 생성 오류",
"hashCopied": "클립보드에 해시 복사",
"errorCopyingHash": "해시 복사 오류",
"token": "토큰",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "파일브라우저 퀀텀에 오신 것을 환영합니다!",
@@ -227,7 +226,6 @@
"renameSuccess": "아이템 이름 변경에 성공했습니다!",
"newFileSuccess": "파일이 성공적으로 생성되었습니다!",
"newDirSuccess": "디렉토리가 성공적으로 생성되었습니다!",
"copyToClipboardFailed": "클립보드에 복사할 수 없습니다.",
"moveFailed": "이동 실패",
"operationFailed": "작업 실패",
"partialSuccess": "일부 작업 실패: {succeeded} 성공, {failed} 실패",
@@ -243,7 +241,7 @@
"unarchiveDestination": "추출 대상:",
"unarchiveSuccess": "아카이브가 성공적으로 추출되었습니다!",
"downloadFailed": "청크 다운로드에 실패했습니다 - 이 문제가 반복되면 청크 크기를 줄여 보세요: {message}",
"confirmPasswordToChangeUserPassword": "비밀번호 변경을 승인하려면 현재 비밀번호를 입력하세요."
"confirmPasswordToSaveUser": "이러한 사용자 변경 사항을 저장하려면 현재 비밀번호를 입력합니다."
},
"search": {
"smallerThan": "보다 작습니다:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP 활성화 성공",
"verificationFailed": "인증 실패",
"verificationSucceed": "인증 성공",
"generationFailed": "세대 실패"
"generationFailed": "세대 실패",
"passwordRequiredForGeneration": "인증 코드를 생성하려면 비밀번호가 필요합니다."
},
"general": {
"name": "이름",
@@ -845,8 +844,6 @@
"showingResults": "{count} 결과 표시",
"noResults": "아이콘을 찾을 수 없습니다.",
"tryDifferentSearch": "다른 검색어를 사용해 보세요.",
"copiedToClipboard": "'{name}' 클립보드에 복사했습니다!",
"copyFailed": "클립보드에 복사하지 못했습니다.",
"custom": "사용자 지정",
"useCustomIcon": "사용자 지정 아이콘을 사용합니다: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Gekopieerd naar klembord!",
"copyFailed": "Kopiëren naar klembord mislukt",
"copyFile": "Bestand kopiëren",
"copyToClipboard": "Kopiëren naar klembord",
"moveFile": "Verplaats bestand",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Selecteer een hash-algoritme",
"generate": "genereren",
"errorGeneratingHash": "Fout bij het genereren van hash",
"hashCopied": "Hash gekopieerd naar klembord",
"errorCopyingHash": "Fout bij het kopiëren van hash",
"token": "Penning",
"onlyOfficeId": "Alleen kantoor-ID",
"firstLoadTitle": "Welkom bij FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Item hernoemd succesvol!",
"newFileSuccess": "Bestand succesvol aangemaakt!",
"newDirSuccess": "Directory succesvol aangemaakt!",
"copyToClipboardFailed": "Kan niet naar klembord kopiëren",
"moveFailed": "Verplaatsen mislukt",
"operationFailed": "Operatie mislukt",
"partialSuccess": "Sommige bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt.",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Uittreksel naar:",
"unarchiveSuccess": "Archief met succes uitgepakt!",
"downloadFailed": "Chunkedownload mislukt - als dit zich herhaalt, probeer dan de grootte van de chunk te verkleinen: {message}",
"confirmPasswordToChangeUserPassword": "Voer uw huidige wachtwoord in om deze wachtwoordwijziging te autoriseren."
"confirmPasswordToSaveUser": "Voer uw huidige wachtwoord in om deze gebruikerswijzigingen op te slaan."
},
"search": {
"smallerThan": "Kleiner dan:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP succesvol ingeschakeld",
"verificationFailed": "Verificatie mislukt",
"verificationSucceed": "Verificatie slagen",
"generationFailed": "Generatie mislukt"
"generationFailed": "Generatie mislukt",
"passwordRequiredForGeneration": "Uw wachtwoord is nodig om een verificatiecode te genereren."
},
"fileLoading": {
"title": "Uploaden en downloads",
@@ -845,8 +844,6 @@
"showingResults": "Resultaten tonen {count} ",
"noResults": "Geen pictogrammen gevonden",
"tryDifferentSearch": "Probeer een andere zoekterm",
"copiedToClipboard": "Gekopieerd '{name}' naar klembord!",
"copyFailed": "Kopiëren naar klembord mislukt",
"custom": "Aangepast",
"useCustomIcon": "Gebruik een aangepast pictogram: {name}",
"iconLibraryUrl": "lettertypen.google.com/iconen",
+16 -19
View File
@@ -57,6 +57,7 @@
"scopes": "Bereiken{suffix}",
"commands": "Opdrachten{suffix}",
"photos": "Foto's",
"audio": "Audio{suffix}",
"videos": "Video{suffix}",
"documents": "Documenten{suffix}",
"archives": "Archieven{suffix}",
@@ -127,13 +128,14 @@
"watching": "Bekijken{suffix}",
"lines": "Regels{suffix}",
"lastUpdate": "Laatste update{suffix}",
"latency": "Vertraging{suffix}",
"audio": "Audio{suffix}"
"latency": "Vertraging{suffix}"
},
"buttons": {
"copySuccess": "Gekopieerd naar klembord!",
"copyFailed": "Kopiëren naar klembord mislukt",
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
"copyFile": "Bestand kopiëren",
"copyPath": "Pad kopiëren",
"copyToClipboard": "Kopiëren naar klembord",
"copyDownloadLinkToClipboard": "Downloadlink kopiëren naar klembord",
"moveFile": "Bestand verplaatsen",
@@ -164,8 +166,7 @@
"keepEditing": "Blijven bewerken",
"closeAnyway": "Sluiten",
"goToItem": "Ga naar item",
"goToSource": "Ga naar de bronlocatie",
"copyPath": "Kopieer pad"
"goToSource": "Ga naar de bronlocatie"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -440,9 +441,6 @@
"selectHashAlgorithm": "Selecteer een hash-algoritme",
"generate": "Genereren",
"errorGeneratingHash": "Fout bij het genereren van hash",
"hashCopied": "Hash gekopieerd naar klembord",
"copyToClipboardFailed": "Kan niet naar klembord kopiëren",
"errorCopyingHash": "Fout bij het kopiëren van hash",
"token": "Token",
"onlyOfficeId": "OnlyOffice-ID",
"firstLoadTitle": "Welkom bij FileBrowser Quantum!",
@@ -451,8 +449,8 @@
"halloweenBody": "Het Halloween-thema is toegepast. U kunt het uitschakelen als u dit niet prettig vindt, of permanent uitschakelen in de profielinstellingen door een ander thema te selecteren.",
"moveFailed": "Verplaatsen mislukt",
"operationFailed": "Bewerking is mislukt",
"confirmPasswordToChangeUserPassword": "Voer uw huidige wachtwoord in om deze wachtwoordwijziging te autoriseren.",
"partialSuccess": "Enkele bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt."
"partialSuccess": "Enkele bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt.",
"confirmPasswordToSaveUser": "Voer uw huidige wachtwoord in om deze gebruikerswijzigingen op te slaan."
},
"search": {
"searchContext": "Zoekcontext: {context}",
@@ -461,15 +459,15 @@
"showOptions": "Opties weergeven",
"notEnoughCharacters": "Niet genoeg tekens om te zoeken (min {minSearchLength})",
"typeToSearch": "Begin met het typen van {minSearchLength} of meer tekens om te beginnen met zoeken.",
"noResults": "Geen resultaten gevonden.",
"smallerThan": "Kleiner dan:",
"largerThan": "Groter dan:",
"olderThanDate": "Ouder dan:",
"newerThanDate": "Nieuwer dan:",
"showPreviewImages": "Voorbeeldafbeeldingen tonen",
"showPreviewImagesDescription": "Toon, indien beschikbaar, miniatuurvoorbeelden voor zoekresultaten en toon grotere pictogrammen voor alle resultaten.",
"helpText2": "Zoekresultaten zijn gebaseerd op geïndexeerde bestanden, dus nieuw toegevoegde bestanden verschijnen mogelijk pas als ze geïndexeerd zijn.",
"noResults": "Geen resultaten gevonden.",
"helpText1": "Er wordt gezocht op elk teken dat je typt (minimaal{minSearchLength} tekens)."
"helpText1": "Er wordt gezocht op elk teken dat je typt (minimaal{minSearchLength} tekens).",
"helpText2": "Zoekresultaten zijn gebaseerd op geïndexeerde bestanden, dus nieuw toegevoegde bestanden verschijnen mogelijk pas als ze geïndexeerd zijn."
},
"profileSettings": {
"preferEditorForMarkdown": "Geef de voorkeur aan editor voor Markdown-bestanden",
@@ -493,6 +491,7 @@
"showHiddenFilesDescription": "Toon verborgen bestanden in de lijst (bijv. .thumbnails, ProgramData), maar ze zijn nog steeds handmatig toegankelijk als ze zijn geconfigureerd om te worden geïndexeerd.",
"showQuickDownload": "Downloadpictogram weergeven voor snelle toegang",
"showQuickDownloadDescription": "Toon een downloadpictogram voor snelle toegang tot elk bestandsitem waarop kan worden geklikt om het meteen te downloaden.",
"previewDescription": "Toon miniatuurvoorbeelden voor {filetype}, toont generiek pictogram indien uitgeschakeld.",
"previewMotion": "Bewegingsvoorbeelden",
"previewMotionVideosDescription": "Bewegingsvoorbeeld is het geanimeerde popup-voorbeeld dat door de frames van de video of meerdere afbeeldingen voor een map loopt",
"editOffice": "Office-bestanden bekijken en bewerken met OnlyOffice",
@@ -513,6 +512,8 @@
"disableSearchOptions": "Zoekopties uitschakelen",
"disableSearchOptionsDescription": "Schakel de zoekopties in de zijbalk uit.",
"hideSidebarFileActions": "Verberg de knop Bestandsacties in de zijbalk",
"showCopyPath": "Pad kopiëren tonen in contextmenu",
"showCopyPathDescription": "Een actie Pad kopiëren weergeven in het contextmenu van bestanden en mappen.",
"deleteWithoutConfirming": "Bestanden verwijderen zonder bevestigingsvenster",
"deleteWithoutConfirmingDescription": "Bestanden verwijderen zonder bevestigingsvenster. Dit werkt alleen als het bestand een mediabestand is.",
"editorViewerOptions": "Opties voor editor en viewer",
@@ -531,10 +532,7 @@
"themeAndLanguage": "Thema & Taal",
"thumbnailOptions": "Miniatuuropties",
"showThumbnails": "Toon miniaturen voor ondersteunde items",
"showThumbnailsDescription": "Als deze optie is ingeschakeld, worden miniaturen weergegeven in de lijst met items die deze ondersteunen. Schakel deze optie uit om alle miniaturen te verbergen.",
"showCopyPath": "Pad kopiëren tonen in contextmenu",
"showCopyPathDescription": "Een actie Pad kopiëren weergeven in het contextmenu van bestanden en mappen.",
"previewDescription": "Toon miniatuurvoorbeelden voor {filetype}, toont generiek pictogram indien uitgeschakeld."
"showThumbnailsDescription": "Als deze optie is ingeschakeld, worden miniaturen weergegeven in de lijst met items die deze ondersteunen. Schakel deze optie uit om alle miniaturen te verbergen."
},
"settings": {
"systemAdmin": "Systeem & Beheer",
@@ -740,7 +738,8 @@
"enabledSuccessfully": "OTP succesvol ingeschakeld",
"verificationFailed": "Verificatie is mislukt",
"verificationSucceed": "Verificatie is geslaagd",
"generationFailed": "Aanmaken is mislukt"
"generationFailed": "Aanmaken is mislukt",
"passwordRequiredForGeneration": "Uw wachtwoord is nodig om een verificatiecode te genereren."
},
"access": {
"all": "Alles weigeren",
@@ -855,8 +854,6 @@
"showingResults": "{count} resultaten weergeven",
"noResults": "Geen pictogrammen gevonden",
"tryDifferentSearch": "Probeer een andere zoekterm",
"copiedToClipboard": "'{name}' is gekopieerd naar klembord!",
"copyFailed": "Kopiëren naar klembord mislukt",
"custom": "Aangepast",
"useCustomIcon": "Gebruik een aangepast pictogram: {name}"
},
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Skopiowano do schowka!",
"copyFailed": "Nie udało się skopiować do schowka",
"copyFile": "Kopiuj plik",
"copyToClipboard": "kopiuj do schowka",
"moveFile": "Przenieś plik",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Wybór algorytmu skrótu",
"generate": "Generowanie",
"errorGeneratingHash": "Błąd generowania skrótu",
"hashCopied": "Hash skopiowany do schowka",
"errorCopyingHash": "Błąd kopiowania skrótu",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Witamy w FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Nazwa elementu została zmieniona pomyślnie!",
"newFileSuccess": "Plik został utworzony pomyślnie!",
"newDirSuccess": "Katalog utworzony pomyślnie!",
"copyToClipboardFailed": "Nie można skopiować do schowka",
"moveFailed": "Przenoszenie nie powiodło się",
"operationFailed": "Operacja nie powiodła się",
"partialSuccess": "Niektóre operacje nie powiodły się: {succeeded} powiodło się, {failed} nie powiodło się",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Wyciąg do:",
"unarchiveSuccess": "Archiwum rozpakowane pomyślnie!",
"downloadFailed": "Pobieranie fragmentów nie powiodło się - jeśli sytuacja się powtórzy, spróbuj zmniejszyć rozmiar fragmentu: {message}",
"confirmPasswordToChangeUserPassword": "Wprowadź bieżące hasło, aby autoryzować zmianę hasła."
"confirmPasswordToSaveUser": "Wprowadź aktualne hasło, aby zapisać zmiany użytkownika."
},
"search": {
"smallerThan": "Mniejszy niż:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP został pomyślnie włączony",
"verificationFailed": "Weryfikacja nie powiodła się",
"verificationSucceed": "Sukces weryfikacji",
"generationFailed": "Generacja nieudana"
"generationFailed": "Generacja nieudana",
"passwordRequiredForGeneration": "Hasło jest wymagane do wygenerowania kodu uwierzytelniającego."
},
"general": {
"name": "Nazwa",
@@ -845,8 +844,6 @@
"showingResults": "Wyświetlanie wyników {count} ",
"noResults": "Nie znaleziono ikon",
"tryDifferentSearch": "Wypróbuj inne wyszukiwane hasło",
"copiedToClipboard": "Skopiowano '{name}' do schowka!",
"copyFailed": "Nie udało się skopiować do schowka",
"custom": "Niestandardowe",
"useCustomIcon": "Użyj niestandardowej ikony: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Copiado para a área de transferência!",
"copyFailed": "Falha ao copiar para a área de transferência",
"copyFile": "Copiar arquivo",
"copyToClipboard": "Copiar",
"moveFile": "Mover arquivo",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Selecione um algoritmo de hash",
"generate": "Gerar",
"errorGeneratingHash": "Erro ao gerar hash",
"hashCopied": "Hash copiado para a área de transferência",
"errorCopyingHash": "Erro ao copiar o hash",
"token": "Token",
"onlyOfficeId": "ID do OnlyOffice",
"firstLoadTitle": "Bem-vindo ao FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Item renomeado com sucesso!",
"newFileSuccess": "Arquivo criado com sucesso!",
"newDirSuccess": "Diretório criado com sucesso!",
"copyToClipboardFailed": "Não é possível copiar para a área de transferência",
"moveFailed": "Falha ao mover",
"operationFailed": "Falha na operação",
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem-sucedido, {failed} falhou",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extrair para:",
"unarchiveSuccess": "Arquivo extraído com sucesso!",
"downloadFailed": "Falha no download em pedaços - se isso se repetir, tente reduzir o tamanho do pedaço: {message}",
"confirmPasswordToChangeUserPassword": "Digite sua senha atual para autorizar a alteração da senha."
"confirmPasswordToSaveUser": "Digite sua senha atual para salvar essas alterações de usuário."
},
"search": {
"smallerThan": "Menor que:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP ativado com sucesso",
"verificationFailed": "Falha na verificação",
"verificationSucceed": "Verificação de sucesso",
"generationFailed": "Falha na geração"
"generationFailed": "Falha na geração",
"passwordRequiredForGeneration": "Sua senha é necessária para gerar um código de autenticação."
},
"general": {
"name": "Nome",
@@ -845,8 +844,6 @@
"showingResults": "Mostrando {count} resultados",
"noResults": "Nenhum ícone encontrado",
"tryDifferentSearch": "Tente um termo de pesquisa diferente",
"copiedToClipboard": "Copiou '{name}' para a área de transferência!",
"copyFailed": "Falha ao copiar para a área de transferência",
"custom": "Personalizado",
"useCustomIcon": "Usar ícone personalizado: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Copiado para a área de transferência!",
"copyFailed": "Falha ao copiar para a área de transferência",
"copyFile": "Copiar ficheiro",
"copyToClipboard": "Copiar",
"moveFile": "Mover ficheiro",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Selecionar um algoritmo de hash",
"generate": "Gerar",
"errorGeneratingHash": "Erro ao gerar hash",
"hashCopied": "Hash copiado para a área de transferência",
"errorCopyingHash": "Erro ao copiar o hash",
"token": "Ficha",
"onlyOfficeId": "ID do OnlyOffice",
"firstLoadTitle": "Bem-vindo ao FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Item renomeado com sucesso!",
"newFileSuccess": "Ficheiro criado com sucesso!",
"newDirSuccess": "Diretório criado com sucesso!",
"copyToClipboardFailed": "Não é possível copiar para a área de transferência",
"moveFailed": "Falha ao mover",
"operationFailed": "A operação falhou",
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem sucedido, {failed} falhou",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extrair para:",
"unarchiveSuccess": "Arquivo extraído com sucesso!",
"downloadFailed": "Falha na transferência em pedaços - se isto se repetir, tente reduzir o tamanho dos pedaços: {message}",
"confirmPasswordToChangeUserPassword": "Introduza a sua palavra-passe atual para autorizar esta alteração de palavra-passe."
"confirmPasswordToSaveUser": "Introduza a sua palavra-passe atual para guardar estas alterações de utilizador."
},
"search": {
"smallerThan": "Mais pequeno do que:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP ativado com sucesso",
"verificationFailed": "Falha na verificação",
"verificationSucceed": "Verificação de sucesso",
"generationFailed": "Geração falhada"
"generationFailed": "Geração falhada",
"passwordRequiredForGeneration": "A sua palavra-passe é necessária para gerar um código de autenticação."
},
"fileLoading": {
"title": "Carregamentos e transferências",
@@ -845,8 +844,6 @@
"showingResults": "Mostrando {count} resultados",
"noResults": "Não foram encontrados ícones",
"tryDifferentSearch": "Experimente um termo de pesquisa diferente",
"copiedToClipboard": "Copiou '{name}' para a área de transferência!",
"copyFailed": "Falha ao copiar para a área de transferência",
"custom": "Personalizado",
"useCustomIcon": "Utilizar um ícone personalizado: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Copiat în clipboard!",
"copyFailed": "Copierea în clipboard a eșuat",
"copyFile": "Copiază fișier",
"copyToClipboard": "Copiază în clipboard",
"moveFile": "Mută fișier",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Selectați un algoritm hash",
"generate": "Generați",
"errorGeneratingHash": "Eroare în generarea hash-ului",
"hashCopied": "Hash copiat în clipboard",
"errorCopyingHash": "Eroare copiere hash",
"token": "Token",
"onlyOfficeId": "NumaiOffice ID",
"firstLoadTitle": "Bine ați venit la FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Element redenumit cu succes!",
"newFileSuccess": "Fișier creat cu succes!",
"newDirSuccess": "Director creat cu succes!",
"copyToClipboardFailed": "Imposibilitatea de a copia în clipboard",
"moveFailed": "Mutarea a eșuat",
"operationFailed": "Operațiunea a eșuat",
"partialSuccess": "Unele operațiuni au eșuat: {succeeded} a reușit, {failed} a eșuat",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extras la:",
"unarchiveSuccess": "Arhiva extrasă cu succes!",
"downloadFailed": "Descărcarea fragmentată a eșuat - dacă acest lucru se repetă, încercați să reduceți dimensiunea fragmentului: {message}",
"confirmPasswordToChangeUserPassword": "Introduceți parola curentă pentru a autoriza această modificare a parolei."
"confirmPasswordToSaveUser": "Introduceți parola curentă pentru a salva aceste modificări de utilizator."
},
"search": {
"smallerThan": "Mai mic decât:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP activat cu succes",
"verificationFailed": "Verificarea a eșuat",
"verificationSucceed": "Verificarea reușită",
"generationFailed": "Generație eșuată"
"generationFailed": "Generație eșuată",
"passwordRequiredForGeneration": "Parola dvs. este necesară pentru a genera un cod de autentificare."
},
"general": {
"name": "Nume și prenume",
@@ -845,8 +844,6 @@
"showingResults": "Afișarea rezultatelor {count} ",
"noResults": "Nu s-au găsit pictograme",
"tryDifferentSearch": "Încercați un alt termen de căutare",
"copiedToClipboard": "Copiat '{name}' în clipboard!",
"copyFailed": "A eșuat copierea în clipboard",
"custom": "Personalizat",
"useCustomIcon": "Utilizați pictograma personalizată: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Скопировано в буфер обмена!",
"copyFailed": "Не удалось скопировать в буфер обмена",
"copyFile": "Скопировать файл",
"copyToClipboard": "Скопировать в буфер",
"moveFile": "Переместить файл",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Выберите алгоритм хэширования",
"generate": "Создать",
"errorGeneratingHash": "Ошибка при генерации хэша",
"hashCopied": "Хэш копируется в буфер обмена",
"errorCopyingHash": "Ошибка при копировании хэша",
"token": "Токен",
"onlyOfficeId": "Идентификатор OnlyOffice",
"firstLoadTitle": "Добро пожаловать в FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Элемент успешно переименован!",
"newFileSuccess": "Файл создан успешно!",
"newDirSuccess": "Каталог создан успешно!",
"copyToClipboardFailed": "Невозможно скопировать в буфер обмена",
"moveFailed": "Перемещение не удалось",
"operationFailed": "Операция не удалась",
"partialSuccess": "Некоторые операции завершились неудачей: {succeeded} - успешно, {failed} - неудачно.",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Отрывок в:",
"unarchiveSuccess": "Архив извлечен успешно!",
"downloadFailed": "Не удалось загрузить чанк - если ситуация повторяется, попробуйте уменьшить размер чанка: {message}",
"confirmPasswordToChangeUserPassword": "Введите свой текущий пароль, чтобы разрешить смену пароля."
"confirmPasswordToSaveUser": "Введите свой текущий пароль, чтобы сохранить изменения пользователя."
},
"search": {
"smallerThan": "Меньше, чем:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP успешно активирован",
"verificationFailed": "Верификация не удалась",
"verificationSucceed": "Проверка удалась",
"generationFailed": "Не удалось создать"
"generationFailed": "Не удалось создать",
"passwordRequiredForGeneration": "Ваш пароль необходим для генерации кода аутентификатора."
},
"general": {
"name": "Имя",
@@ -845,8 +844,6 @@
"showingResults": "Показать результаты {count} ",
"noResults": "Значки не найдены",
"tryDifferentSearch": "Попробуйте использовать другой поисковый запрос",
"copiedToClipboard": "Скопировано '{name}' в буфер обмена!",
"copyFailed": "Не удалось скопировать в буфер обмена",
"custom": "Пользовательское",
"useCustomIcon": "Используйте пользовательский значок: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Skopírované do schránky!",
"copyFailed": "Kopírovanie do schránky zlyhalo",
"copyFile": "Kopírovať súbor",
"copyToClipboard": "Kopírovať do schránky",
"moveFile": "Presunúť súbory",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Výber algoritmu hash",
"generate": "Generovať",
"errorGeneratingHash": "Chyba pri generovaní hash",
"hashCopied": "Hash skopírovaný do schránky",
"errorCopyingHash": "Chyba pri kopírovaní hash",
"token": "Token",
"onlyOfficeId": "IbaOffice ID",
"firstLoadTitle": "Vitajte v programe FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Položka bola úspešne premenovaná!",
"newFileSuccess": "Súbor bol úspešne vytvorený!",
"newDirSuccess": "Adresár bol úspešne vytvorený!",
"copyToClipboardFailed": "Nemožnosť kopírovania do schránky",
"moveFailed": "Presun zlyhal",
"operationFailed": "Operácia zlyhala",
"partialSuccess": "Niektoré operácie zlyhali: {succeeded} sa podarilo, {failed} sa nepodarilo",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Výpis na:",
"unarchiveSuccess": "Archív úspešne extrahovaný!",
"downloadFailed": "Sťahovanie po častiach zlyhalo - ak sa to opakuje, skúste zmenšiť veľkosť časti: {message}",
"confirmPasswordToChangeUserPassword": "Zadajte svoje aktuálne heslo, aby ste autorizovali túto zmenu hesla."
"confirmPasswordToSaveUser": "Zadajte svoje aktuálne heslo, aby ste tieto zmeny používateľa uložili."
},
"search": {
"smallerThan": "Menšie ako:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP úspešne povolený",
"verificationFailed": "Overenie sa nepodarilo",
"verificationSucceed": "Overovanie Úspech",
"generationFailed": "Generácia zlyhala"
"generationFailed": "Generácia zlyhala",
"passwordRequiredForGeneration": "Na vygenerovanie autentifikačného kódu je potrebné vaše heslo."
},
"general": {
"name": "Názov",
@@ -845,8 +844,6 @@
"showingResults": "Zobrazené výsledky {count} ",
"noResults": "Nenašli sa žiadne ikony",
"tryDifferentSearch": "Skúste použiť iný vyhľadávací výraz",
"copiedToClipboard": "Skopírované '{name}' do schránky!",
"copyFailed": "Nepodarilo sa skopírovať do schránky",
"custom": "Vlastné",
"useCustomIcon": "Použite vlastnú ikonu: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Kopierat till urklipp!",
"copyFailed": "Misslyckades med att kopiera till urklipp",
"copyFile": "Kopiera fil",
"copyToClipboard": "Kopiera till urklipp",
"moveFile": "Flytta fil",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Välj en hashalgoritm",
"generate": "Generera",
"errorGeneratingHash": "Fel vid generering av hash",
"hashCopied": "Hash kopieras till urklipp",
"errorCopyingHash": "Fel vid kopiering av hash",
"token": "Token",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Välkommen till FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Objektet har bytt namn framgångsrikt!",
"newFileSuccess": "Filen skapades framgångsrikt!",
"newDirSuccess": "Katalog skapad framgångsrikt!",
"copyToClipboardFailed": "Det går inte att kopiera till urklipp",
"moveFailed": "Flyttning misslyckades",
"operationFailed": "Operationen misslyckades",
"partialSuccess": "Vissa operationer misslyckades: {succeeded} lyckades, {failed} misslyckades",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Extrahera till:",
"unarchiveSuccess": "Arkivet har extraherats framgångsrikt!",
"downloadFailed": "Nedladdningen av chunked misslyckades - om detta upprepas, försök att minska chunkstorleken: {message}",
"confirmPasswordToChangeUserPassword": "Ange ditt nuvarande lösenord för att godkänna denna lösenordsändring."
"confirmPasswordToSaveUser": "Ange ditt nuvarande lösenord för att spara dessa användarändringar."
},
"search": {
"smallerThan": "Mindre än:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP aktiverat framgångsrikt",
"verificationFailed": "Verifiering misslyckades",
"verificationSucceed": "Verifiering lyckas",
"generationFailed": "Generation misslyckad"
"generationFailed": "Generation misslyckad",
"passwordRequiredForGeneration": "Ditt lösenord krävs för att generera en autentiseringskod."
},
"fileLoading": {
"title": "Uppladdningar och nedladdningar",
@@ -845,8 +844,6 @@
"showingResults": "Visar resultat från {count} ",
"noResults": "Inga ikoner hittades",
"tryDifferentSearch": "Försök med en annan sökterm",
"copiedToClipboard": "Kopierade '{name}' till urklipp!",
"copyFailed": "Misslyckades med att kopiera till klippbordet",
"custom": "Anpassad",
"useCustomIcon": "Använd anpassad ikon: {name}",
"iconLibraryUrl": "teckensnitt.google.com/ikoner",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Panoya kopyalandı!",
"copyFailed": "Panoya kopyalama başarısız",
"copyFile": "Dosyayı kopyala",
"copyToClipboard": "Panoya kopyala",
"moveFile": "Dosyayı taşı",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Bir karma algoritma seçin",
"generate": "Oluşturmak",
"errorGeneratingHash": "Hash oluştururken hata oluştu",
"hashCopied": "Panoya kopyalanan karma",
"errorCopyingHash": "Karma kopyalama hatası",
"token": "Jeton",
"onlyOfficeId": "SadeceOfis Kimliği",
"firstLoadTitle": "FileBrowser Quantum'a hoş geldiniz!",
@@ -227,7 +226,6 @@
"renameSuccess": "Öğe başarıyla yeniden adlandırıldı!",
"newFileSuccess": "Dosya başarıyla oluşturuldu!",
"newDirSuccess": "Dizin başarıyla oluşturuldu!",
"copyToClipboardFailed": "Panoya kopyalanamıyor",
"moveFailed": "Taşıma başarısız",
"operationFailed": "İşlem başarısız oldu",
"partialSuccess": "Bazı işlemler başarısız oldu: {succeeded} başarılı, {failed} başarısız",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Özetle:",
"unarchiveSuccess": "Arşiv başarıyla çıkarıldı!",
"downloadFailed": "Yığın indirme başarısız oldu - bu tekrarlanırsa yığın boyutunu küçültmeyi deneyin: {message}",
"confirmPasswordToChangeUserPassword": "Bu şifre değişikliğini onaylamak için mevcut şifrenizi girin."
"confirmPasswordToSaveUser": "Bu kullanıcı değişikliklerini kaydetmek için mevcut şifrenizi girin."
},
"search": {
"smallerThan": "Daha küçük:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP başarıyla etkinleştirildi",
"verificationFailed": "Doğrulama Başarısız",
"verificationSucceed": "Doğrulama Başarılı",
"generationFailed": "Nesil Başarısız"
"generationFailed": "Nesil Başarısız",
"passwordRequiredForGeneration": "Kimlik doğrulayıcı kodu oluşturmak için parolanız gereklidir."
},
"general": {
"name": "İsim",
@@ -845,8 +844,6 @@
"showingResults": "{count} sonuçları gösteriliyor",
"noResults": "Simge bulunamadı",
"tryDifferentSearch": "Farklı bir arama terimi deneyin",
"copiedToClipboard": "'{name}' panoya kopyalandı!",
"copyFailed": "Panoya kopyalanamadı",
"custom": "Özel",
"useCustomIcon": "Özel simge kullanın: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "Скопійовано в буфер обміну!",
"copyFailed": "Не вдалося скопіювати в буфер обміну",
"copyFile": "Копіювати файл",
"copyToClipboard": "Копіювати в буфер обміну",
"moveFile": "Перемістити файл",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "Виберіть алгоритм хешування",
"generate": "Згенерувати",
"errorGeneratingHash": "Хеш, що генерує помилки",
"hashCopied": "Хеш скопійовано до буфера обміну",
"errorCopyingHash": "Помилка копіювання хешу",
"token": "Токен.",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "Ласкаво просимо до FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "Елемент перейменовано успішно!",
"newFileSuccess": "Файл успішно створено!",
"newDirSuccess": "Каталог створено успішно!",
"copyToClipboardFailed": "Неможливо скопіювати в буфер обміну",
"moveFailed": "Переміщення не вдалося",
"operationFailed": "Операція не вдалася",
"partialSuccess": "Деякі операції не вдалися: {succeeded} вдалося, {failed} не вдалося",
@@ -243,7 +241,7 @@
"unarchiveDestination": "Витягнути до:",
"unarchiveSuccess": "Архів успішно розпаковано!",
"downloadFailed": "Не вдалося завантажити фрагмент - якщо це повториться, спробуйте зменшити розмір фрагмента: {message}",
"confirmPasswordToChangeUserPassword": "Введіть ваш поточний пароль, щоб дозволити зміну пароля."
"confirmPasswordToSaveUser": "Введіть ваш поточний пароль, щоб зберегти зміни користувача."
},
"search": {
"smallerThan": "Менше, ніж:",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "OTP успішно ввімкнено",
"verificationFailed": "Перевірка не вдалася",
"verificationSucceed": "Перевірка пройшла успішно",
"generationFailed": "Покоління зазнало невдачі"
"generationFailed": "Покоління зазнало невдачі",
"passwordRequiredForGeneration": "Ваш пароль необхідний для генерації коду автентифікатора."
},
"fileLoading": {
"title": "Завантаження та вивантаження",
@@ -845,8 +844,6 @@
"showingResults": "Показує результати {count} ",
"noResults": "Не знайдено жодної піктограми",
"tryDifferentSearch": "Спробуйте інший пошуковий запит",
"copiedToClipboard": "Скопіюйте '{name}' в буфер обміну!",
"copyFailed": "Не вдалося скопіювати в буфер обміну",
"custom": "Нестандартний",
"useCustomIcon": "Використовуйте власну іконку: {name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "已复制到剪贴板!",
"copyFailed": "复制到剪贴板失败",
"copyFile": "复制文件",
"copyToClipboard": "复制到剪贴板",
"moveFile": "移动文件",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "选择散列算法",
"generate": "生成",
"errorGeneratingHash": "错误生成哈希值",
"hashCopied": "哈希值复制到剪贴板",
"errorCopyingHash": "复制哈希值出错",
"token": "代币",
"onlyOfficeId": "唯一办公室 ID",
"firstLoadTitle": "欢迎访问 FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "项目重命名成功!",
"newFileSuccess": "文件创建成功!",
"newDirSuccess": "目录创建成功!",
"copyToClipboardFailed": "无法复制到剪贴板",
"moveFailed": "移动失败",
"operationFailed": "操作失败",
"partialSuccess": "某些操作失败:{succeeded} 成功,{failed} 失败",
@@ -243,7 +241,7 @@
"unarchiveDestination": "摘录至",
"unarchiveSuccess": "档案提取成功!",
"downloadFailed": "分块下载失败 - 如果重复出现这种情况,请尝试减小分块大小:{message}",
"confirmPasswordToChangeUserPassword": "输入您的当前密码以授权密码更改。"
"confirmPasswordToSaveUser": "输入您当前的密码以保存这些用户更改。"
},
"search": {
"smallerThan": "小于",
@@ -544,7 +542,8 @@
"enabledSuccessfully": "成功启用 OTP",
"verificationFailed": "验证失败",
"verificationSucceed": "验证成功",
"generationFailed": "一代失败"
"generationFailed": "一代失败",
"passwordRequiredForGeneration": "生成验证码需要您输入密码。"
},
"fileLoading": {
"title": "上传和下载",
@@ -845,8 +844,6 @@
"showingResults": "显示{count} 结果",
"noResults": "未找到图标",
"tryDifferentSearch": "尝试不同的搜索词",
"copiedToClipboard": "将 '{name}' 复制到剪贴板!",
"copyFailed": "复制到剪贴板失败",
"custom": "定制",
"useCustomIcon": "使用自定义图标:{name}",
"iconLibraryUrl": "fonts.google.com/icons",
+4 -7
View File
@@ -1,6 +1,7 @@
{
"buttons": {
"copySuccess": "已複製到剪貼簿!",
"copyFailed": "複製到剪貼簿失敗",
"copyFile": "複製檔案",
"copyToClipboard": "複製到剪貼簿",
"moveFile": "移動檔案",
@@ -215,8 +216,6 @@
"selectHashAlgorithm": "選擇雜湊演算法",
"generate": "產生",
"errorGeneratingHash": "產生雜湊時出錯",
"hashCopied": "已將雜湊值複製到剪貼簿",
"errorCopyingHash": "複製雜湊時出錯",
"token": "權杖",
"onlyOfficeId": "OnlyOffice ID",
"firstLoadTitle": "歡迎來到 FileBrowser Quantum!",
@@ -227,7 +226,6 @@
"renameSuccess": "項目已成功重新命名!",
"newFileSuccess": "檔案已成功建立!",
"newDirSuccess": "目錄建立成功!",
"copyToClipboardFailed": "無法複製到剪貼簿",
"moveFailed": "移動失敗",
"operationFailed": "操作失敗",
"partialSuccess": "部分作業失敗:成功 {succeeded} 項,失敗 {failed} 項",
@@ -243,7 +241,7 @@
"unarchiveDestination": "解壓縮至:",
"unarchiveSuccess": "封存檔解壓縮成功!",
"downloadFailed": "分塊下載失敗 - 如果此情況重複發生,請嘗試減少分塊大小:{message}",
"confirmPasswordToChangeUserPassword": "輸入您目前的密碼以授權此密碼變更。"
"confirmPasswordToSaveUser": "輸入您目前的密碼以儲存這些使用者變更。"
},
"search": {
"smallerThan": "小於:",
@@ -609,7 +607,8 @@
"enabledSuccessfully": "已成功啟用雙因素驗證",
"verificationFailed": "驗證失敗",
"verificationSucceed": "驗證成功",
"generationFailed": "產生驗證碼失敗"
"generationFailed": "產生驗證碼失敗",
"passwordRequiredForGeneration": "生成驗證碼需要您的密碼。"
},
"fileLoading": {
"title": "上傳與下載",
@@ -845,8 +844,6 @@
"showingResults": "顯示 {count} 筆結果",
"noResults": "找不到圖示",
"tryDifferentSearch": "嘗試不同的搜尋字詞",
"copiedToClipboard": "已將「{name}」複製到剪貼簿!",
"copyFailed": "複製到剪貼簿失敗",
"custom": "自訂",
"useCustomIcon": "使用自訂圖示:{name}",
"iconLibraryUrl": "fonts.google.com/icons",
+2
View File
@@ -15,6 +15,8 @@ import i18n from "@/i18n";
const titles = {
Login: i18n.global.t("general.login"),
Share: i18n.global.t("general.share"),
/** Same component as Share; name differs for /public optional-auth routes. */
PublicShare: i18n.global.t("general.share"),
Files: i18n.global.t("general.files"),
Tools: i18n.global.t("tools.title"),
ChildTool: i18n.global.t("tools.title"),
+36 -15
View File
@@ -160,25 +160,46 @@ export const mutations = {
emitStateChanged();
},
setSources: (user) => {
const currentSource = user.scopes.length > 0 ? user.scopes[0].name : "";
// Rebuilding sources from /api/users must not wipe stats already merged from
// /api/settings/sources, SSE, or a prior validateLogin — otherwise the sidebar
// shows "unknown"/red until a full page reload (common after OTP: initAuth then
// router beforeResolve both refresh the user).
const sameUser = Boolean(state.user && user.username === state.user.username);
const prevInfo = sameUser && state.sources?.info ? state.sources.info : {};
let currentSource = user.scopes.length > 0 ? user.scopes[0].name : "";
if (
sameUser &&
state.sources?.current &&
user.scopes.some((s) => s.name === state.sources.current)
) {
currentSource = state.sources.current;
}
let sources = {info: {}, current: currentSource, count: user.scopes.length};
for (const source of user.scopes) {
const prev = prevInfo[source.name];
const merge = Boolean(prev);
sources.info[source.name] = {
pathPrefix: sources.count == 1 ? "" : encodeURIComponent(source.name),
used: 0,
total: 0,
usedAlt: 0,
usedPercentage: 0,
status: "unknown",
name: source.name,
files: 0,
folders: 0,
lastIndex: 0,
quickScanDurationSeconds: 0,
fullScanDurationSeconds: 0,
complexity: 0,
used: merge ? prev.used : 0,
total: merge ? prev.total : 0,
usedAlt: merge ? prev.usedAlt : 0,
usedPercentage: merge ? prev.usedPercentage : 0,
status: merge ? prev.status : "unknown",
name: merge ? prev.name : source.name,
files: merge ? prev.files : 0,
folders: merge ? prev.folders : 0,
lastIndex: merge ? prev.lastIndex : 0,
quickScanDurationSeconds: merge ? prev.quickScanDurationSeconds : 0,
fullScanDurationSeconds: merge ? prev.fullScanDurationSeconds : 0,
complexity: merge ? prev.complexity : 0,
scanners: merge && prev.scanners ? [...prev.scanners] : [],
};
}
// Sidebar usage bar uses hasSourceInfo + per-source used/total; must survive object replace
sources.hasSourceInfo = user.scopes.some((s) => {
const e = sources.info[s.name];
return e && (e.total > 0 || e.used > 0 || e.usedAlt > 0);
});
// Check if user has custom sidebar links with sources
let targetSource = sources.current;
if (state.user?.sidebarLinks && state.user.sidebarLinks.length > 0) {
@@ -200,7 +221,7 @@ export const mutations = {
}
state.user.gallerySize = value
emitStateChanged();
usersApi.update(state.user, ['gallerySize']);
void usersApi.update(state.user, ['gallerySize']).catch((e) => notify.showError(e));
},
setActiveSettingsView: (value) => {
if (value == state.activeSettingsView) {
@@ -535,7 +556,7 @@ export const mutations = {
value.id = state.user.id;
value.username = state.user?.username;
if (updatedProperties.length > 0) {
usersApi.update(value, updatedProperties);
void usersApi.update(value, updatedProperties).catch((e) => notify.showError(e));
}
}
// Emit state change event
-1
View File
@@ -101,7 +101,6 @@ export async function logout() {
export async function initAuth() {
if (!getters.isShare()) {
console.log("validating login");
await validateLogin();
}
if (globalVars.recaptcha) {
+44
View File
@@ -0,0 +1,44 @@
// Copy to clipboard.
// Uses clipboard API when available, and will fallback to execCommand (which is deprecated, but will make it work on insecure connections)
import i18n from '@/i18n';
import { notify } from '@/notify';
/**
* @param {string} text - Text to copy.
* @returns {Promise<boolean>} True if copy succeeded, false otherwise.
*/
export async function copyToClipboard(text) {
const successMessage = i18n.global.t('buttons.copySuccess');
const errorMessage = i18n.global.t('buttons.copyFailed');
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
notify.showSuccessToast(successMessage);
return true;
}
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const success = document.execCommand('copy');
document.body.removeChild(textArea);
if (success) {
notify.showSuccessToast(successMessage);
return true;
} else {
throw new Error('execCommand returned false');
}
} catch (err) {
console.error('Copy failed:', err);
notify.showError(errorMessage);
return false;
}
}
+2 -1
View File
@@ -84,10 +84,11 @@ export default {
if (!state.sessionId) {
mutations.setSession(generateRandomCode(8));
}
// Set up toast callback
// Set up toast callback (sync any toasts that were queued before Layout mounted, e.g. OTP on login)
notify.setToastUpdateCallback((toasts) => {
this.toasts = toasts;
});
this.toasts = notify.getToasts();
this.reEval()
this.initialize();
},
+1 -1
View File
@@ -356,7 +356,7 @@ export default {
},
});
}
if (e.message.includes("OTP is enforced, but user is not yet configured")) {
if (e.message.includes("OTP is enforced, but user is not configured")) {
mutations.showPrompt({
name: "totp",
pinned: true,
-2
View File
@@ -126,8 +126,6 @@ export default defineComponent({
listing = [state.req]; // Fallback to current item only
}
} else {
// Shouldn't happen, but fallback to current item
console.error("No listing found DocViewer.vue");
listing = [state.req];
}
+8 -3
View File
@@ -68,7 +68,14 @@ export default {
// Check if state and route are synchronized
isStateSynced() {
if (this.viewerMode) return true;
if (!this.routeFilename || !this.originalReq) return false;
if (!this.originalReq || !this.req) return false;
if (getters.isShare()) {
const subPath = state.shareInfo?.subPath;
if (subPath === undefined || subPath === null) return false;
if (!url.pathsMatch(this.req.path, subPath)) return false;
return url.pathsMatch(this.originalReq.path, this.req.path);
}
if (!this.routeFilename) return false;
return this.originalReq.name === this.routeFilename;
},
// Editor content to display
@@ -284,8 +291,6 @@ export default {
listing = [this.req];
}
} else {
console.error("No listing found Editor.vue");
// Shouldn't happen, but fallback to current item
listing = [this.req];
}
+42 -54
View File
@@ -732,19 +732,6 @@ export default {
return;
}
}
// Handle the space bar key
if (key === " " && !modifierKeys) {
event.preventDefault();
if (state.isSearchActive) {
mutations.setSearch(false);
mutations.closeHovers();
} else {
mutations.setSearch(true);
}
}
if (getters.currentPromptName()) {
return;
}
let currentPath = url.removeTrailingSlash(state.route.path);
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
@@ -1000,7 +987,8 @@ export default {
from: item.from,
fromSource: item.fromSource,
to: destPath + item.name,
toSource: state.req.source
toSource: state.req.source,
name: item.name,
}));
const operation = this.clipboard.key === "x" ? "move" : "copy";
@@ -1011,51 +999,51 @@ export default {
props: {
operation: operation,
items: items,
onConfirm: async () => {
mutations.setLoading("listing", true);
onConfirm: () => {
return new Promise((resolve, reject) => {
let action = async (overwrite, rename) => {
try {
if (getters.isShare()) {
await resourcesApi.moveCopyPublic(state.shareInfo.hash, items, operation, overwrite, rename);
} else {
await resourcesApi.moveCopy(items, operation, overwrite, rename);
const action = async (overwrite, rename) => {
try {
if (getters.isShare()) {
await resourcesApi.moveCopyPublic(state.shareInfo.hash, items, operation, overwrite, rename);
} else {
await resourcesApi.moveCopy(items, operation, overwrite, rename);
}
if (operation === "move") {
this.clipboard = { items: [] };
this.internalClipboardTimestamp = 0;
}
mutations.setReload(true);
resolve();
} catch (error) {
console.error("Error moving/copying items:", error);
reject(error);
}
if (operation === "move") {
this.clipboard = { items: [] };
this.internalClipboardTimestamp = 0;
}
mutations.setLoading("listing", false);
} catch (error) {
console.error("Error moving/copying items:", error);
} finally {
mutations.setLoading("listing", false);
mutations.setReload(true);
};
if (this.clipboard.path === state.route.path) {
action(false, true);
return;
}
};
if (this.clipboard.path === state.route.path) {
action(false, true);
return;
}
const conflict = upload.checkConflict(items, state.req.items);
const conflict = upload.checkConflict(items, state.req.items);
if (conflict) {
mutations.showPrompt({
name: "replace-rename",
pinned: true,
confirm: (event, option) => {
const overwrite = option === "overwrite";
const rename = option === "rename";
event.preventDefault();
mutations.closeTopPrompt();
action(overwrite, rename);
},
});
return;
}
action(false, false);
if (conflict) {
mutations.showPrompt({
name: "replace-rename",
pinned: true,
confirm: (event, option) => {
const overwrite = option === "overwrite";
const rename = option === "rename";
event.preventDefault();
mutations.closeTopPrompt();
action(overwrite, rename);
},
});
return;
}
action(false, false);
});
},
},
});
+3 -29
View File
@@ -12,7 +12,7 @@ import { marked } from "marked";
import DOMPurify from 'dompurify';
import { state, mutations, getters } from "@/store";
import hljs from 'highlight.js';
import { notify } from "@/notify";
import { copyToClipboard } from "@/utils/clipboard";
export default {
name: "markdownViewer",
@@ -99,37 +99,11 @@ export default {
}, 1500);
};
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
// Using clipboard API
navigator.clipboard.writeText(text).then(() => {
showFeedback(true);
notify.showSuccessToast(this.$t('buttons.copySuccess'));
}).catch((err) => {
console.error('Clipboard API error:', err);
showFeedback(false);
notify.showErrorToast(this.$t('tools.materialIconPicker.copyFailed'));
});
} else {
// Fallback using execCommand.
// This seems the only way to allow copy from http (insecure) connections (even if is marked as deprecated)
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
showFeedback(true);
notify.showSuccessToast(this.$t('buttons.copySuccess'));
} else {
showFeedback(false);
notify.showErrorToast(this.$t('tools.materialIconPicker.copyFailed'));
}
}
const success = await copyToClipboard(text);
showFeedback(success);
} catch (err) {
console.error('Copy failed:', err);
showFeedback(false);
notify.showErrorToast(this.$t('tools.materialIconPicker.copyFailed'));
}
});
wrapper.appendChild(copyButton);
+5 -9
View File
@@ -35,13 +35,8 @@
/>
</div>
<div v-else-if="previewType == 'pdf'" class="pdf-wrapper">
<iframe class="pdf" :src="raw"></iframe>
<a v-if="isMobileSafari" :href="raw" target="_blank" class="button button--flat floating-btn">
<div>
<i class="material-symbols">open_in_new</i>{{ $t("general.openFile") }}
</div>
</a>
<div v-else-if="isPdf" class="pdf-wrapper">
<iframe allow="web-share" class="pdf" :src="raw"></iframe>
</div>
<div v-else class="info">
@@ -166,6 +161,9 @@ export default {
}
return getters.previewType();
},
isPdf() {
return state.req.type === 'application/pdf';
},
raw() {
const isHeicOrHeif = state.req.type === "image/heic" || state.req.type === "image/heif";
@@ -457,8 +455,6 @@ export default {
this.listing = [state.req];
}
} else {
console.error("No listing found Preview.vue");
// Shouldn't happen, but fallback to current item
this.listing = [state.req];
}
}
+4 -22
View File
@@ -47,7 +47,7 @@
</button>
</td>
<td class="small">
<button class="action copy-clipboard" :data-clipboard-text="link.token"
<button class="action" @click.stop="copyToClipboard(link.token)"
:aria-label="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')">
<i class="material-symbols">content_paste</i>
</button>
@@ -65,10 +65,9 @@
</template>
<script>
import { notify } from "@/notify";
import { authApi } from "@/api";
import { state, mutations, getters } from "@/store";
import Clipboard from "clipboard";
import { copyToClipboard } from "@/utils/clipboard";
import Errors from "@/views/Errors.vue";
import { eventBus } from "@/store/eventBus";
@@ -81,7 +80,6 @@ export default {
return {
error: null,
links: [],
clip: null,
user: {
permissions: { ...state.user.permissions}
},
@@ -93,14 +91,10 @@ export default {
mounted() {
// Listen for API key changes
eventBus.on('apiKeysChanged', this.reloadApiKeys);
this.initClipboard();
},
beforeUnmount() {
// Clean up event listener
eventBus.off('apiKeysChanged', this.reloadApiKeys);
if (this.clip) {
this.clip.destroy();
}
},
computed: {
settings() {
@@ -114,16 +108,8 @@ export default {
},
},
methods: {
initClipboard() {
// Destroy existing clipboard first
if (this.clip) {
this.clip.destroy();
}
// Create new clipboard instance
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
notify.showSuccessToast(this.$t("success.linkCopied")); // Use consistent message
});
async copyToClipboard(text) {
await copyToClipboard(text);
},
async reloadApiKeys() {
mutations.setLoading("api", true);
@@ -131,10 +117,6 @@ export default {
// Fetch the API keys from the specified endpoint
this.links = await authApi.getApiKeys();
this.error = null; // Clear errors
// Reinitialize clipboard after data changes
this.$nextTick(() => {
this.initClipboard();
});
} catch (e) {
// ignore 404 errors
if (e.status !== 404) {
+5 -4
View File
@@ -376,13 +376,14 @@ export default {
"debugOffice",
"preferEditorForMarkdown",
]);
if (themeChanged) {
window.location.reload();
}
notify.showSuccessToast(
this.$t('settings.settingsUpdated')
);
if (themeChanged) {
setTimeout(() => {
window.location.reload();
}, 1000);
}
} catch (e) {
console.error(e);
}
+6 -27
View File
@@ -51,13 +51,13 @@
</button>
</td>
<td class="small">
<button class="action copy-clipboard" :data-clipboard-text="buildLink(item)"
<button class="action" @click.stop="copyToClipboard(buildLink(item))"
:aria-label="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')">
<i class="material-symbols">content_paste</i>
</button>
</td>
<td class="small">
<button :disabled="item.shareType == 'upload'" class="action copy-clipboard" :data-clipboard-text="item.downloadURL" v-if="item.downloadURL"
<button :disabled="item.shareType == 'upload'" class="action" @click.stop="copyToClipboard(item.downloadURL)" v-if="item.downloadURL"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')">
<i class="material-symbols">content_paste_go</i>
</button>
@@ -76,10 +76,10 @@
import { notify } from "@/notify";
import { shareApi } from "@/api";
import { state, mutations, getters } from "@/store";
import Clipboard from "clipboard";
import Errors from "@/views/Errors.vue";
import { fromNow } from '@/utils/moment';
import { eventBus } from "@/store/eventBus";
import { copyToClipboard } from "@/utils/clipboard";
export default {
name: "shares",
@@ -92,23 +92,16 @@ export default {
error: null,
/** @type {any[]} */
links: [],
/** @type {any} */
clip: null,
};
},
async created() {
await this.reloadShares();
},
mounted() {
this.initClipboard();
// Listen for share changes
eventBus.on('sharesChanged', this.reloadShares);
},
beforeUnmount() {
// Clean up clipboard
if (this.clip) {
this.clip.destroy();
}
// Clean up event listener
eventBus.off('sharesChanged', this.reloadShares);
},
@@ -127,6 +120,9 @@ export default {
},
},
methods: {
async copyToClipboard(text) {
await copyToClipboard(text);
},
async reloadShares() {
mutations.setLoading("shares", true);
try {
@@ -137,9 +133,6 @@ export default {
}
this.links = links;
this.error = null; // Clear any previous errors
this.$nextTick(() => {
this.initClipboard();
});
} catch (e) {
this.error = e;
console.error(e);
@@ -147,20 +140,6 @@ export default {
mutations.setLoading("shares", false);
}
},
initClipboard() {
// First destroy any existing clipboard
if (this.clip) {
this.clip.destroy();
}
// Create new clipboard
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
notify.showSuccessToast(this.$t("success.linkCopied"));
});
this.clip.on("error", () => {
notify.showErrorToast(this.$t("prompts.copyToClipboardFailed"));
});
},
editLink(item) {
mutations.showPrompt({
name: "share",
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more