Compare commits

..
8 Commits
Author SHA1 Message Date
NotAlexNoyle 53411da695 Exclude CODEOWNERS from language statistics 2026-11-01 10:10:00 -05:00
NotAlexNoyle e3bbadc419 Add CODEOWNERS (one staff approval) 2026-11-01 10:10:00 -05:00
Graham Steffaniak 37596b1e23 Update README.md (#1667) 2025-11-26 19:04:24 -06:00
d35fb56587 Beta/v1.1.1 (#1655)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
2025-11-24 15:01:49 -06:00
Graham Steffaniak 5dab4c7b30 Update README.md (#1624) 2025-11-19 14:30:37 -06:00
a33dbfc464 Beta/v1.1.0 (#1614)
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
2025-11-19 08:50:49 -06:00
18738b3273 Stable/v1.0.1 (#1559)
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
2025-11-10 10:57:17 -06:00
Ironic 1d8c74dbd9 Remove unstable version warning from README (#1512) 2025-11-04 08:05:18 -05:00
79 changed files with 1243 additions and 628 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+21
View File
@@ -2,6 +2,27 @@
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.1.1-beta
**Notes**:
- [docker] upgraded ffmpeg 8.0 to 8.0.1
- stricter access control checks on file downloads.
- Wrong translation on Chinese "save" button #1650
- Incorrect Spanish Translations in Folder Creation and Rename Actions #1626
- duplicate file detector has stricter partial checksum match #1617
**BugFixes**:
- added missing exiftool to docker image for heic conversion orientation support
- v1.1.0-beta - Incorrect naming of 1 file in directory-info #1621
- disable only office viewing settings not applying
- OnlyOffice integration does not work behind proxy authentication #1422
- Newly created users "add on" to defined scope of previous user #1628 #1518
- disable chown on upload / file saving #1469 #1546
- Uploading a file will silently overwrite any existing file with the same name #1564
- share file url issue
- Fix drag and drop and rows in normal view #1651
- Some text based files are not able to edit. #1567
## v1.1.0-beta
**New Features**:
+3 -1
View File
@@ -2,7 +2,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/gtsteffaniak/filebrowser/backend)](https://goreportcard.com/report/github.com/gtsteffaniak/filebrowser/backend)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8)](https://www.codacy.com/gh/gtsteffaniak/filebrowser/dashboard)
[![latest version](https://img.shields.io/github/release/gtsteffaniak/filebrowser/all.svg)](https://github.com/gtsteffaniak/filebrowser/releases)
[![latest version](https://img.shields.io/github/v/release/gtsteffaniak/filebrowser)](https://github.com/gtsteffaniak/filebrowser/releases)
[![DockerHub Pulls](https://img.shields.io/docker/pulls/gtstef/filebrowser?label=latest%20Docker%20pulls)](https://hub.docker.com/r/gtstef/filebrowser)
[![Apache-2.0 License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -17,6 +17,8 @@
## Pinned
:loudspeaker: [What's Coming Soon](https://github.com/gtsteffaniak/filebrowser/discussions/1622)
:pushpin: [Read The Official Docs](https://filebrowserquantum.com/) (currently english-only)
## About
+3 -2
View File
@@ -1,4 +1,4 @@
FROM gtstef/ffmpeg:8.0-decode AS ffmpeg
FROM gtstef/ffmpeg:8.0.1-decode AS ffmpeg
FROM golang:alpine AS base
ARG VERSION
ARG REVISION
@@ -28,7 +28,7 @@ COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
ENV PATH="$PATH:/home/filebrowser"
RUN apk --no-cache add ca-certificates mailcap tzdata curl
RUN apk --no-cache add ca-certificates mailcap tzdata curl exiftool
RUN adduser -D -s /bin/true -u 1000 filebrowser
USER filebrowser
WORKDIR /home/filebrowser
@@ -42,6 +42,7 @@ COPY --from=nbuild --chown=filebrowser:1000 [ "/app/dist/", "./http/dist/" ]
RUN [ "filebrowser", "version" ]
RUN [ "ffmpeg", "-version" ]
RUN [ "ffprobe", "-version" ]
RUN [ "exiftool", "-ver" ]
USER root
# exposing default port for auto discovery.
EXPOSE 80
+2 -2
View File
@@ -6,7 +6,7 @@ tmp_dir = "tmp"
bin = "tmp/filebrowser"
full_bin = "FILEBROWSER_DEVMODE=true CGO_ENABLED=1 ./tmp/filebrowser -c test_config.yaml"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["tmp", "vendor", "http/dist", "http/embed", "swagger/docs"]
exclude_dir = ["tmp", "vendor", "http/dist", "http/embed", "swagger/docs", "preview/thumbnails", "preview/heic"]
log = "air.log"
delay = 1000 # ms
@@ -14,4 +14,4 @@ tmp_dir = "tmp"
time = true
[misc]
clean_on_exit = true
clean_on_exit = false
+1 -1
View File
@@ -18,4 +18,4 @@ tmp_dir = "tmp"
time = true
[misc]
clean_on_exit = true
clean_on_exit = false
+138
View File
@@ -0,0 +1,138 @@
package files
import (
"os"
"path/filepath"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/require"
)
func TestGetContent_UTF8Truncation(t *testing.T) {
// Get the path to the test file
// The test file is in frontend/tests/playwright-files/utf8-truncated.txt
// We need to find it relative to the test execution directory
cwd, err := os.Getwd()
require.NoError(t, err)
// Navigate from backend/adapters/fs/files to the project root
// backend/adapters/fs/files -> backend/adapters/fs -> backend/adapters -> backend -> root
testFilePath := filepath.Join(cwd, "..", "..", "..", "..", "frontend", "tests", "playwright-files", "utf8-truncated.txt")
// Check if file exists, if not try alternative path
if _, err = os.Stat(testFilePath); os.IsNotExist(err) {
// Try from project root
testFilePath = filepath.Join("frontend", "tests", "playwright-files", "utf8-truncated.txt")
if _, err = os.Stat(testFilePath); os.IsNotExist(err) {
t.Skipf("Test file not found at %s, skipping test", testFilePath)
return
}
}
// Get absolute path
var absPath string
absPath, err = filepath.Abs(testFilePath)
require.NoError(t, err)
t.Run("file with UTF-8 truncation at 4096 byte boundary", func(t *testing.T) {
// The test file is longer than 4096 bytes, with valid UTF-8 throughout.
// However, when reading exactly 4096 bytes as a header, it cuts off in the
// middle of a multi-byte UTF-8 sequence (e6 9c, missing the last byte 88 of '月').
// This should trigger the truncation handling in getContent's header validation.
// The fix should trim the incomplete sequence from the header, allowing the
// header check to pass, and then the full file (which is valid UTF-8) should
// pass the full file validation.
content, err := getContent(absPath)
// Should not return an error - the header truncation is handled, and the full file is valid
require.NoError(t, err)
// The content should be the full valid UTF-8 file
require.NotEmpty(t, content, "Content should not be empty - full file is valid UTF-8")
// Verify it contains the expected text
require.Contains(t, content, "文件已备份", "Content should contain Chinese characters")
require.Contains(t, content, "2024年", "Content should contain date information")
})
t.Run("regular UTF-8 text file", func(t *testing.T) {
// Test with a simple text file to ensure normal files still work
tmpFile, err := os.CreateTemp("", "test-utf8-*.txt")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
testText := "Hello, 世界! This is a test file with UTF-8 characters.\n"
_, err = tmpFile.WriteString(testText)
require.NoError(t, err)
tmpFile.Close()
content, err := getContent(tmpFile.Name())
require.NoError(t, err)
require.Equal(t, testText, content)
})
t.Run("file smaller than header size", func(t *testing.T) {
// Test with a file smaller than 4096 bytes
tmpFile, err := os.CreateTemp("", "test-small-*.txt")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
testText := "Small file content"
_, err = tmpFile.WriteString(testText)
require.NoError(t, err)
tmpFile.Close()
content, err := getContent(tmpFile.Name())
require.NoError(t, err)
require.Equal(t, testText, content)
})
t.Run("file with Chinese characters at boundary", func(t *testing.T) {
// Create a file that's exactly 4094 bytes, ending with a complete Chinese character
tmpFile, err := os.CreateTemp("", "test-chinese-*.txt")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
// Create content that's exactly 4094 bytes, ending with complete UTF-8
baseText := "2024年 06月 17日 星期一 04:05:58 CST 文件已备份\n"
content := ""
for len([]byte(content)) < 4094 {
content += baseText
}
// Trim to exactly 4094 bytes
encoded := []byte(content)
if len(encoded) > 4094 {
encoded = encoded[:4094]
}
// Ensure it ends with a complete character by finding the last complete rune
for len(encoded) > 0 {
lastRune, _ := decodeLastRune(encoded)
if lastRune != 0xFFFD { // RuneError
break
}
encoded = encoded[:len(encoded)-1]
}
_, err = tmpFile.Write(encoded)
require.NoError(t, err)
tmpFile.Close()
result, err := getContent(tmpFile.Name())
require.NoError(t, err)
require.NotEmpty(t, result)
require.Equal(t, string(encoded), result)
})
}
// Helper function to check if last rune is valid
func decodeLastRune(p []byte) (rune, int) {
if len(p) == 0 {
return 0, 0
}
r, size := utf8.DecodeLastRune(p)
return r, size
}
+34 -18
View File
@@ -550,17 +550,12 @@ func WriteDirectory(opts utils.FileOptions) error {
}
// Ensure the parent directories exist
// Permissions are set by MkdirAll (subject to umask, which is usually acceptable)
err = os.MkdirAll(realPath, fileutils.PermDir)
if err != nil {
return err
}
// Explicitly set directory permissions to bypass umask
err = os.Chmod(realPath, fileutils.PermDir)
if err != nil {
return err
}
return RefreshIndex(idx.Name, opts.Path, true, true)
}
@@ -580,15 +575,20 @@ func WriteFile(source, path string, in io.Reader) error {
}
var stat os.FileInfo
// Check if the destination exists and is a directory
if stat, err = os.Stat(realPath); err == nil && 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)
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)
}
}
// If file exists, its permissions will be preserved (O_TRUNC doesn't change permissions)
}
// 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)
file, err := os.OpenFile(realPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileutils.PermFile)
if err != nil {
return err
@@ -601,12 +601,6 @@ 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 {
return err
}
return RefreshIndex(source, path, false, false)
}
@@ -637,9 +631,31 @@ func getContent(realPath string) (string, error) {
// --- Start of new heuristic checks ---
if n > 0 {
// Trim header to last complete UTF-8 rune to avoid false negatives
// when the header read cuts off in the middle of a multi-byte sequence.
// We decode runes from the end until we find a valid one, trimming
// any incomplete sequences at the end.
trimmedHeader := actualHeader
for len(trimmedHeader) > 0 {
lastRune, size := utf8.DecodeLastRune(trimmedHeader)
if lastRune != utf8.RuneError {
// Found a valid complete rune
break
}
// RuneError occurred - this could be an incomplete sequence or invalid byte
// Trim the last byte and try again
if size == 1 && len(trimmedHeader) > 0 {
trimmedHeader = trimmedHeader[:len(trimmedHeader)-1]
} else {
// Shouldn't happen, but break to avoid infinite loop
break
}
}
// 1. Basic Check: Is the header valid UTF-8?
// If not, it's unlikely an editable UTF-8 text file.
if !utf8.Valid(actualHeader) {
// Use trimmed header to avoid false negatives from truncated sequences
if len(trimmedHeader) > 0 && !utf8.Valid(trimmedHeader) {
return "", nil // Not an error, just not the text file we want
}
+7 -3
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
)
@@ -31,11 +32,14 @@ func MakeUserDirs(u *users.User, disableScopeChange bool) error {
if filepath.Base(scope.Scope) != cleanedUserName && source.Config.CreateUserDir && !disableScopeChange {
fullPath := filepath.Join(source.Path, scope.Scope, cleanedUserName)
parentDir := filepath.Join(source.Path, scope.Scope)
// validate that scope path exists
// If parent directory doesn't exist and createUserDir is enabled, create it
if !Exists(parentDir) {
return fmt.Errorf("MakeUserDirs: scope path does not exist: %s", scope.Scope)
if err := MakeUserDir(parentDir); err != nil {
return fmt.Errorf("MakeUserDirs: failed to create parent scope directory: %s - %v", scope.Scope, err)
}
}
scope.Scope = filepath.Join(scope.Scope, cleanedUserName)
// Use JoinPathAsUnix to ensure scope remains in Unix format (forward slashes)
scope.Scope = utils.JoinPathAsUnix(scope.Scope, cleanedUserName)
err := MakeUserDir(fullPath)
if err != nil {
return fmt.Errorf("MakeUserDirs: failed to create user home dir: %s", err)
+15 -6
View File
@@ -62,6 +62,13 @@ func CopyFile(source, dest string) error {
// copySingleFile handles copying a single file.
func copySingleFile(source, dest string) error {
// Get source file info to preserve permissions
srcInfo, err := os.Stat(source)
if err != nil {
return err
}
sourcePerms := srcInfo.Mode().Perm()
// Open the source file.
src, err := os.Open(source)
if err != nil {
@@ -75,8 +82,8 @@ func copySingleFile(source, dest string) error {
return err
}
// Create the destination file.
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, PermFile)
// Create the destination file with source permissions
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, sourcePerms)
if err != nil {
return err
}
@@ -88,11 +95,13 @@ func copySingleFile(source, dest string) error {
return err
}
// Set the configured file permissions instead of copying from source
err = os.Chmod(dest, PermFile)
// Preserve source file permissions
// Handle chmod errors gracefully (e.g., in rootless containers where chmod may be restricted)
err = os.Chmod(dest, sourcePerms)
if err != nil {
return err
// Log but don't fail - chmod may be restricted in some environments
// The file was copied successfully, so we continue
logger.Debugf("Could not set file permissions for %s (this may be expected in restricted environments): %v", dest, err)
}
return nil
+1 -1
View File
@@ -57,7 +57,7 @@ type Server struct {
ExternalUrl string `json:"externalUrl"` // used by share links if set (eg. http://mydomain.com)
InternalUrl string `json:"internalUrl"` // used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080)
CacheDir string `json:"cacheDir"` // path to the cache directory, used for thumbnails and other cached files
CacheDirCleanup *bool `json:"cacheDirCleanup"` // whether to automatically cleanup the cache directory (default: true)
CacheDirCleanup *bool `json:"cacheDirCleanup"` // whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)
MaxArchiveSizeGB int64 `json:"maxArchiveSize"` // max pre-archive combined size of files/folder that are allowed to be archived (in GB)
Filesystem Filesystem `json:"filesystem"` // filesystem settings
// not exposed to config
+1
View File
@@ -25,6 +25,7 @@ type CommonShare struct {
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
Path string `json:"path,omitempty"`
DownloadURL string `json:"downloadURL,omitempty"`
ShareURL string `json:"shareURL,omitempty"`
DisableShareCard bool `json:"disableShareCard,omitempty"`
EnforceDarkLightMode string `json:"enforceDarkLightMode,omitempty"` // "dark" or "light"
ViewMode string `json:"viewMode,omitempty"` // default view mode for anonymous users: "list", "compact", "normal", "gallery"
+2 -3
View File
@@ -14,13 +14,13 @@ require (
github.com/google/go-cmp v0.7.0
github.com/gtsteffaniak/go-cache v1.0.0
github.com/gtsteffaniak/go-logger v0.2.3
github.com/kovidgoyal/imaging v1.8.15
github.com/kovidgoyal/imaging v1.7.2
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.44.0
golang.org/x/crypto v0.45.0
golang.org/x/image v0.33.0
golang.org/x/mod v0.30.0
golang.org/x/oauth2 v0.33.0
@@ -161,7 +161,6 @@ require (
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
+4 -6
View File
@@ -546,10 +546,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.15 h1:FwKRGvmbQ04OFYF5WIalcvUontcnj1chBeHmR5I7UYg=
github.com/kovidgoyal/imaging v1.8.15/go.mod h1:aXZHGm/gKcaXpxn1NnQiVtuz3Z2BCnXWyjF0ZGrmJ1g=
github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c=
github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -893,8 +891,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+51 -25
View File
@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
@@ -48,13 +49,14 @@ type duplicatesOptions struct {
// duplicatesHandler handles requests to find duplicate files
//
// This endpoint finds files with the same size and uses fuzzy filename matching
// to verify potential duplicates.
// This endpoint finds files with the same size, uses fuzzy filename matching
// for initial grouping (fast), then verifies all final groups with checksums.
//
// It's optimized to handle large directories efficiently by:
// 1. Filtering by minimum size first
// 2. Grouping by size
// 3. Verifying matches with fuzzy filename similarity
// 3. Initial grouping with fuzzy filename similarity (fast)
// 4. Final verification with checksums on all groups (accurate)
//
// @Summary Find Duplicate Files
// @Description Finds duplicate files based on size and fuzzy filename matching
@@ -79,7 +81,8 @@ func duplicatesHandler(w http.ResponseWriter, r *http.Request, d *requestContext
}
// Generate cache key from all input parameters that affect results
cacheKey := fmt.Sprintf("%s:%s:%d:%t", index.Path, opts.combinedPath, opts.minSize, opts.useChecksum)
// Checksums are always enabled, so cache key doesn't need to include that flag
cacheKey := fmt.Sprintf("%s:%s:%d", index.Path, opts.combinedPath, opts.minSize)
// Check cache first (before acquiring mutex)
if cachedResults, ok := duplicateResultsCache.Get(cacheKey); ok {
@@ -132,8 +135,12 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
}
})
// Step 2: Process each size group to find duplicates
duplicateGroups := []duplicateGroup{}
// Step 2: Process each size group to find duplicates using filename matching (fast)
// We'll verify with checksums later on the final groups
candidateGroups := []struct {
size int64
locations []fileLocation
}{}
totalFiles := 0
for size, locations := range sizeGroups {
@@ -141,14 +148,10 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
continue // Skip files with no potential duplicates
}
var groups [][]fileLocation
if opts.useChecksum {
groups = groupLocationsByChecksum(locations, index, size)
} else {
groups = groupLocationsByFilename(locations, index, size)
}
// Use filename matching for initial grouping (fast)
groups := groupLocationsByFilename(locations, index, size)
// Step 3: Only now create SearchResult objects for actual duplicates
// Collect candidate groups up to the limit
for _, locGroup := range groups {
if len(locGroup) < 2 {
continue
@@ -159,7 +162,31 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
break
}
// Create SearchResult objects only for these confirmed duplicates
candidateGroups = append(candidateGroups, struct {
size int64
locations []fileLocation
}{size, locGroup})
totalFiles += len(locGroup)
}
// Stop processing more size groups if we've hit the limit
if totalFiles >= maxTotalFiles {
break
}
}
// Step 3: Verify all final groups with checksums
duplicateGroups := []duplicateGroup{}
for _, candidate := range candidateGroups {
// Verify with checksums
verifiedGroups := groupLocationsByChecksum(candidate.locations, index, candidate.size)
// Create SearchResult objects only for verified duplicates
for _, locGroup := range verifiedGroups {
if len(locGroup) < 2 {
continue
}
resultGroup := make([]*indexing.SearchResult, 0, len(locGroup))
index.ReadOnlyOperation(func() {
dirs := index.GetDirectories()
@@ -175,7 +202,7 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
// CRITICAL: Verify size matches the expected size for this group
// Index could have changed between initial grouping and now
if file.Size != size {
if file.Size != candidate.size {
continue
}
@@ -200,20 +227,19 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
if len(resultGroup) >= 2 {
duplicateGroups = append(duplicateGroups, duplicateGroup{
Size: size,
Size: candidate.size,
Count: len(resultGroup),
Files: resultGroup,
})
totalFiles += len(resultGroup)
}
}
// Stop processing more size groups if we've hit the limit
if totalFiles >= maxTotalFiles {
break
}
}
// Sort groups by size (largest to smallest)
sort.Slice(duplicateGroups, func(i, j int) bool {
return duplicateGroups[i].Size > duplicateGroups[j].Size
})
return duplicateGroups
}
@@ -221,9 +247,9 @@ func prepDuplicatesOptions(r *http.Request, d *requestContext) (*duplicatesOptio
source := r.URL.Query().Get("source")
scope := r.URL.Query().Get("scope")
minSizeMbStr := r.URL.Query().Get("minSizeMb")
// Checksum feature disabled due to performance issues with large file systems
// Always use fuzzy filename matching instead
useChecksum := false // r.URL.Query().Get("useChecksum") == "true"
// Checksums are always enabled by default for final verification
// First pass uses filename matching for speed, then checksums verify final groups
useChecksum := true
// Default minimum size: 1MB
minSizeMb := int64(1)
+11 -1
View File
@@ -126,7 +126,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
link.IncrementUserDownload(data.user.Username)
}
}
file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/")
file.Path = utils.AddTrailingSlashIfNotExists(path)
// Set the file info in the `data` object
data.fileInfo = *file
// Call the next handler with the data
@@ -395,6 +395,16 @@ func getProxyUser(w http.ResponseWriter, r *http.Request, data *requestContext,
if data.user.Username == "" {
return http.StatusForbidden, errors.ErrUnauthorized
}
// Generate a token for proxy users if they don't have one
if data.token == "" {
expires := time.Hour * time.Duration(config.Auth.TokenExpirationHours)
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions)
if err != nil {
logger.Errorf("Failed to generate token for proxy user %s: %v", proxyUser, err)
return http.StatusInternalServerError, fmt.Errorf("failed to generate token")
}
data.token = signed.Key
}
// Call the handler function, passing in the context (or return OK if no handler)
if fn == nil {
return http.StatusOK, nil
+57 -10
View File
@@ -179,10 +179,10 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
sendOnlyOfficeLogEvent(logContext, "INFO", "config", fmt.Sprintf("OnlyOffice session started for document: %s ", path))
// Build download URL that OnlyOffice server will use
downloadURL := buildOnlyOfficeDownloadURL(source, providedPath, d.fileInfo.Hash, d.token)
downloadURL := buildOnlyOfficeDownloadURL(r, source, providedPath, d.fileInfo.Hash, d.token)
// Build callback URL for OnlyOffice to notify us of changes
callbackURL := buildOnlyOfficeCallbackURL(source, providedPath, d.fileInfo.Hash, d.token)
callbackURL := buildOnlyOfficeCallbackURL(r, source, providedPath, d.fileInfo.Hash, d.token)
// Build OnlyOffice client configuration
clientConfig := map[string]interface{}{
@@ -228,14 +228,37 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
}
// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
func buildOnlyOfficeDownloadURL(r *http.Request, source, path, hash, token string) string {
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
baseURL := settings.Config.Server.BaseURL
var baseURL string
if settings.Config.Server.InternalUrl != "" {
// Ensure proper URL joining without double slashes
// InternalUrl is a full URL (e.g., http://localhost:8080), so use it directly
internalURL := strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
baseURLPath := strings.TrimPrefix(settings.Config.Server.BaseURL, "/")
baseURL = internalURL + "/" + baseURLPath
if baseURLPath != "" {
baseURL = internalURL + "/" + baseURLPath
} else {
baseURL = internalURL
}
} else {
// Extract scheme and host from request (respecting X-Forwarded-* headers)
var host string
var scheme string
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
// Use X-Forwarded-Proto if available, otherwise default to https for proxied requests
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
scheme = forwardedProto
} else {
scheme = "https"
}
} else {
// Fallback to simple approach
host = r.Host
scheme = getScheme(r)
}
baseURL = fmt.Sprintf("%s://%s%s", scheme, host, settings.Config.Server.BaseURL)
}
var downloadURL string
@@ -255,13 +278,37 @@ func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
}
// buildOnlyOfficeCallbackURL constructs the callback URL that OnlyOffice server will use to notify us of changes
func buildOnlyOfficeCallbackURL(source, path, hash, token string) string {
baseURL := settings.Config.Server.BaseURL
func buildOnlyOfficeCallbackURL(r *http.Request, source, path, hash, token string) string {
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
var baseURL string
if settings.Config.Server.InternalUrl != "" {
// Ensure proper URL joining without double slashes
// InternalUrl is a full URL (e.g., http://localhost:8080), so use it directly
internalURL := strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
baseURLPath := strings.TrimPrefix(settings.Config.Server.BaseURL, "/")
baseURL = internalURL + "/" + baseURLPath
if baseURLPath != "" {
baseURL = internalURL + "/" + baseURLPath
} else {
baseURL = internalURL
}
} else {
// Extract scheme and host from request (respecting X-Forwarded-* headers)
var host string
var scheme string
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
// Use X-Forwarded-Proto if available, otherwise default to https for proxied requests
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
scheme = forwardedProto
} else {
scheme = "https"
}
} else {
// Fallback to simple approach
host = r.Host
scheme = getScheme(r)
}
baseURL = fmt.Sprintf("%s://%s%s", scheme, host, settings.Config.Server.BaseURL)
}
var callbackURL string
+50 -13
View File
@@ -111,20 +111,20 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
if idx == nil {
return fmt.Errorf("source %s is not available", source)
}
if d.share != nil {
_, err = files.FileInfoFaster(utils.FileOptions{
Path: path,
Source: source,
Expand: false,
}, nil)
} else {
_, err = files.FileInfoFaster(utils.FileOptions{
Username: d.user.Username,
Path: path,
Source: source,
Expand: false,
}, store.Access)
// Check access control directly for each file and silently skip if access is denied
if d.share == nil && store.Access != nil {
if !store.Access.Permitted(idx.Path, path, d.user.Username) {
return nil // Silently skip this file/folder
}
}
// Verify file exists
_, err = files.FileInfoFaster(utils.FileOptions{
Path: path,
Source: source,
Expand: false,
}, nil)
if err != nil {
return err
}
@@ -158,6 +158,23 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
return nil
}
// Check access control for each file/folder during walk
if d.share == nil {
// Construct the index-relative path for this file/folder
// relPath is relative to realPath, so we need to join it with the original path
indexRelPath := filepath.Join(path, relPath)
indexRelPath = filepath.ToSlash(indexRelPath) // Normalize separators
if !store.Access.Permitted(idx.Path, indexRelPath, d.user.Username) {
// Skip this file/folder silently
if fileInfo.IsDir() {
// Skip the entire directory by returning filepath.SkipDir
return filepath.SkipDir
}
return nil
}
}
// Prepend base folder name unless flatten is true
if !flatten {
relPath = filepath.Join(baseName, relPath)
@@ -322,6 +339,19 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
}
}
// Verify access control before opening the file (direct rule check)
if d.share == nil && store.Access != nil {
if !store.Access.Permitted(idx.Path, firstFilePath, d.user.Username) {
logger.Debugf("user %s denied access to path %s", d.user.Username, firstFilePath)
// Send OnlyOffice error log if this was an OnlyOffice download
if isOnlyOffice && logContext != nil {
sendOnlyOfficeLogEvent(logContext, "ERROR", "download",
fmt.Sprintf("OnlyOffice download failed - access denied by rule: %s", firstFilePath))
}
return http.StatusForbidden, fmt.Errorf("access denied to path %s", firstFilePath)
}
}
fd, err2 := os.Open(realPath)
if err2 != nil {
// Send OnlyOffice error log if this was an OnlyOffice download
@@ -480,6 +510,12 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
}
path = utils.JoinPathAsUnix(userScope, path)
// Check access control for each file in the archive
// Silently skip if access is denied (as if the file doesn't exist)
if store.Access != nil && !store.Access.Permitted(idx.Path, path, d.user.Username) {
continue // Skip this file and continue with the next one
}
}
// For shares, the path is already correctly resolved by publicRawHandler
realPath, isDir, err := idx.GetRealPath(path)
@@ -512,6 +548,7 @@ func createZip(d *requestContext, tmpDirPath string, filenames ...string) error
for _, fname := range filenames {
err := addFile(fname, d, nil, zipWriter, false)
if err != nil {
// Access control failures return nil, so any error here is a real error
logger.Errorf("Failed to add %s to ZIP: %v", fname, err)
return err
}
+6 -1
View File
@@ -382,7 +382,12 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
}
fileInfo, err := files.FileInfoFaster(fileOpts, accessStore)
if err != nil {
if err == nil { // File exists
if r.URL.Query().Get("override") != "true" {
logger.Debugf("resource already exists: %v", fileInfo.RealPath)
return http.StatusConflict, nil
}
// If overriding, delete existing thumbnails
preview.DelThumbs(r.Context(), *fileInfo)
}
err = files.WriteFile(fileOpts.Source, fileOpts.Path, r.Body)
+30 -17
View File
@@ -26,10 +26,9 @@ import (
// ShareResponse represents a share with computed username field and download URL
type ShareResponse struct {
*share.Link
Source string `json:"source"` // Override embedded field to show source name
Username string `json:"username,omitempty"`
DownloadURL string `json:"downloadURL,omitempty"`
PathExists bool `json:"pathExists"`
Source string `json:"source"` // Override embedded field to show source name
Username string `json:"username,omitempty"`
PathExists bool `json:"pathExists"`
}
// convertToFrontendShareResponse converts shares to response format with usernames
@@ -63,14 +62,15 @@ func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*S
pathExists := utils.CheckPathExists(filepath.Join(sourceInfo.Path, s.Path))
s.CommonShare.HasPassword = s.HasPassword()
s.DownloadURL = getShareURL(r, s.Hash, true)
s.ShareURL = getShareURL(r, s.Hash, false)
// Create response with source name (overrides the embedded Link's source field)
responses = append(responses, &ShareResponse{
Link: s,
Source: sourceInfo.Name, // Override to show source name instead of backend path
Username: username,
DownloadURL: getDownloadURL(r, s.Hash),
PathExists: pathExists,
Link: s,
Source: sourceInfo.Name, // Override to show source name instead of backend path
Username: username,
PathExists: pathExists,
})
}
return responses, nil
@@ -392,6 +392,7 @@ type DirectDownloadResponse struct {
Status string `json:"status"`
Hash string `json:"hash"`
DownloadURL string `json:"url"`
ShareURL string `json:"shareUrl"`
}
// shareDirectDownloadHandler creates a direct download link for files only.
@@ -509,7 +510,8 @@ func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *reque
response := DirectDownloadResponse{
Status: "201",
Hash: existing.Hash,
DownloadURL: getDownloadURL(r, existing.Hash),
DownloadURL: getShareURL(r, existing.Hash, true),
ShareURL: getShareURL(r, existing.Hash, false),
}
return renderJSON(w, r, response)
}
@@ -540,21 +542,26 @@ func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *reque
response := DirectDownloadResponse{
Status: "200",
Hash: secureHash,
DownloadURL: getDownloadURL(r, secureHash),
DownloadURL: getShareURL(r, secureHash, true),
ShareURL: getShareURL(r, secureHash, false),
}
return renderJSON(w, r, response)
}
func getDownloadURL(r *http.Request, hash string) string {
var downloadURL string
func getShareURL(r *http.Request, hash string, isDirectDownload bool) string {
var shareURL string
if config.Server.ExternalUrl != "" {
downloadURL = fmt.Sprintf("%s%spublic/api/raw?hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, hash)
if isDirectDownload {
shareURL = fmt.Sprintf("%s%spublic/api/raw?hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, hash)
} else {
shareURL = fmt.Sprintf("%s%spublic/share/%s", config.Server.ExternalUrl, config.Server.BaseURL, hash)
}
} else {
// Prefer X-Forwarded-Host for proxy support
var host string
var scheme string
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
// Use X-Forwarded-Proto if available, otherwise default to https for proxied requests
@@ -568,9 +575,13 @@ func getDownloadURL(r *http.Request, hash string) string {
host = r.Host
scheme = getScheme(r)
}
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", scheme, host, config.Server.BaseURL, hash)
if isDirectDownload {
shareURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", scheme, host, config.Server.BaseURL, hash)
} else {
shareURL = fmt.Sprintf("%s://%s%spublic/share/%s", scheme, host, config.Server.BaseURL, hash)
}
}
return downloadURL
return shareURL
}
// shareInfoHandler retrieves share information by hash.
@@ -590,6 +601,8 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err != nil {
return http.StatusNotFound, fmt.Errorf("share hash not found")
}
commonShare.DownloadURL = getShareURL(r, hash, true)
commonShare.ShareURL = getShareURL(r, hash, false)
return renderJSON(w, r, commonShare)
}
+8 -40
View File
@@ -233,11 +233,15 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
}
if !dirInfo.IsDir() {
// Use handleFile for consistent size calculation across platforms
realSize, _ := idx.handleFile(dirInfo, adjustedPath, realPath)
size := int64(realSize)
fileInfo := iteminfo.FileInfo{
Path: adjustedPath,
ItemInfo: iteminfo.ItemInfo{
Name: filepath.Base(originalPath),
Size: dirInfo.Size(),
Size: size,
ModTime: dirInfo.ModTime(),
},
}
@@ -376,8 +380,9 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
idx.incrementScannerDirs()
}
} else {
size, shouldCountSize := idx.handleFile(file, fullCombined)
itemInfo.DetectType(realPath+"/"+file.Name(), false)
realFilePath := realPath + "/" + file.Name()
size, shouldCountSize := idx.handleFile(file, fullCombined, realFilePath)
itemInfo.DetectType(realFilePath, false)
// Set HasPreview flags - use cached metadata optimization only when indexing is enabled
usedCachedPreview := false
if !idx.Config.DisableIndexing && config.Recursive {
@@ -758,43 +763,6 @@ func (idx *Index) SetStatus(status IndexStatus) error {
return idx.SendSourceUpdateEvent()
}
func (idx *Index) handleFile(file os.FileInfo, fullCombined string) (size uint64, shouldCountSize bool) {
var realSize uint64
var nlink uint64 = 1
var ino uint64 = 0
canUseSyscall := false
if sys := file.Sys(); sys != nil {
realSize, nlink, ino, canUseSyscall = getFileDetails(sys)
}
if !canUseSyscall {
// Fallback for non-unix systems or if syscall info is unavailable
realSize = uint64(file.Size())
}
if nlink > 1 {
// It's a hard link
idx.mu.Lock()
defer idx.mu.Unlock()
if _, exists := idx.processedInodes[ino]; exists {
// Already seen, don't count towards global total, or directory total.
return realSize, false
}
// First time seeing this inode.
idx.processedInodes[ino] = struct{}{}
idx.FoundHardLinks[fullCombined] = realSize
idx.totalSize += realSize
return realSize, true // Count size for directory total.
}
// It's a regular file.
idx.mu.Lock()
idx.totalSize += realSize
idx.mu.Unlock()
return realSize, true // Count size.
}
// input should be non-index path.
func (idx *Index) MakeIndexPath(path string) string {
if path == "." || strings.HasPrefix(path, "./") {
-5
View File
@@ -56,11 +56,6 @@ var ResizableImageTypes = map[string]bool{
".tif": true,
".heic": true,
".heif": true,
".webp": true,
".pbm": true,
".pgm": true,
".ppm": true,
".pam": true,
".other": false,
}
+71 -7
View File
@@ -3,8 +3,11 @@
package indexing
import (
"os"
"strings"
"syscall"
"github.com/gtsteffaniak/go-logger/logger"
)
func CheckWindowsHidden(realpath string) bool {
@@ -24,14 +27,75 @@ func getPartitionSize(path string) (uint64, error) {
return total, nil
}
func getFileDetails(sys any) (uint64, uint64, uint64, bool) {
if stat, ok := sys.(*syscall.Stat_t); ok {
// Use actual file size, not allocated blocks
// This matches what ls -l shows and what FileInfoFaster returns
realSize := uint64(stat.Size)
return realSize, uint64(stat.Nlink), stat.Ino, true
func getFileDetails(sys any, filePath string) (uint64, uint64, uint64, bool) {
// On Unix, we should always be able to get syscall info
// First try from file.Sys() if available (fast path)
if sys != nil {
if stat, ok := sys.(*syscall.Stat_t); ok {
// Use allocated size for `du`-like behavior
realSize := uint64(stat.Blocks * 512)
return realSize, uint64(stat.Nlink), stat.Ino, true
}
}
return 0, 1, 0, false
// If file.Sys() didn't work, try direct stat syscall
// On Unix, we should always have a file path and be able to stat
if filePath == "" {
// No file path available - this is an error condition on Unix
// We cannot proceed without syscall info on Unix
return 0, 1, 0, false
}
// Perform direct stat syscall - this should always work on Unix if file exists
var stat syscall.Stat_t
err := syscall.Stat(filePath, &stat)
if err != nil {
// On Unix, if stat fails, the file likely doesn't exist or we don't have permission
// This is an error condition - we cannot proceed without syscall info on Unix
return 0, 1, 0, false
}
// Use allocated size for `du`-like behavior
realSize := uint64(stat.Blocks * 512)
return realSize, uint64(stat.Nlink), stat.Ino, true
}
// handleFile processes a file and returns its size and whether it should be counted
// On Unix, always uses syscall to get allocated size (du-like behavior)
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string) (size uint64, shouldCountSize bool) {
var realSize uint64
var nlink uint64
var ino uint64
canUseSyscall := false
sys := file.Sys()
realSize, nlink, ino, canUseSyscall = getFileDetails(sys, realFilePath)
if !canUseSyscall {
logger.Errorf("Failed to get syscall info for file %s on Unix system - file may have been deleted or permission denied. Using file.Size() fallback.", realFilePath)
realSize = uint64(file.Size())
}
if nlink > 1 {
// It's a hard link
idx.mu.Lock()
defer idx.mu.Unlock()
if _, exists := idx.processedInodes[ino]; exists {
// Already seen, don't count towards global total, or directory total.
return realSize, false
}
// First time seeing this inode.
idx.processedInodes[ino] = struct{}{}
idx.FoundHardLinks[fullCombined] = realSize
idx.totalSize += realSize
return realSize, true // Count size for directory total.
}
// It's a regular file.
idx.mu.Lock()
idx.totalSize += realSize
idx.mu.Unlock()
return realSize, true // Count size.
}
// platform specific rules
+16 -1
View File
@@ -3,6 +3,7 @@
package indexing
import (
"os"
"strings"
"golang.org/x/sys/windows"
@@ -46,13 +47,27 @@ func getPartitionSize(path string) (uint64, error) {
return totalBytes, nil
}
func getFileDetails(sys any) (uint64, uint64, uint64, bool) {
func getFileDetails(sys any, filePath string) (uint64, uint64, uint64, bool) {
// On Windows, os.FileInfo.Sys() returns *syscall.Win32FileAttributeData,
// which does not contain inode or link count information.
// We return false to indicate that we should use the fallback mechanism.
// filePath parameter is ignored on Windows
return 0, 1, 0, false
}
// handleFile processes a file and returns its size and whether it should be counted
// On Windows, uses file.Size() directly (no syscall support for allocated size)
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string) (size uint64, shouldCountSize bool) {
// On Windows, just use the actual file size
realSize := uint64(file.Size())
// Windows doesn't support hard links in the same way, so always count size
idx.mu.Lock()
idx.totalSize += realSize
idx.mu.Unlock()
return realSize, true
}
// input should be non-index path.
func (idx *Index) MakeIndexPathPlatform(path string) string {
split := strings.Split(path, "\\")
+1 -42
View File
@@ -21,11 +21,6 @@ gif
tiff
bmp
heic
webp
pbm
pgm
ppm
pam
)
*/
type Format int
@@ -44,16 +39,6 @@ func (x Format) toImaging() imaging.Format {
return imaging.BMP
case FormatHeic:
return imaging.JPEG
case FormatWebp:
return imaging.WEBP
case FormatPbm:
return imaging.PBM
case FormatPgm:
return imaging.PGM
case FormatPpm:
return imaging.PPM
case FormatPam:
return imaging.PAM
default:
return imaging.JPEG
}
@@ -111,16 +96,6 @@ func (s *Service) FormatFromExtension(ext string) (Format, error) {
return FormatTiff, nil
case imaging.BMP:
return FormatBmp, nil
case imaging.WEBP:
return FormatWebp, nil
case imaging.PBM:
return FormatPbm, nil
case imaging.PGM:
return FormatPgm, nil
case imaging.PPM:
return FormatPpm, nil
case imaging.PAM:
return FormatPam, nil
default:
return -1, ErrUnsupportedFormat
}
@@ -191,25 +166,9 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
}
// For HEIC files, try without AutoOrientation first since it might not work properly
// Try decoding with default settings (auto color space conversion)
img, err := imaging.Decode(wrappedReader)
if err != nil {
// If decoding fails due to ICC profile issues (e.g., unsupported grayscale colorspace),
// retry with color space conversion disabled
if bytes.Contains([]byte(err.Error()), []byte("colorspace")) ||
bytes.Contains([]byte(err.Error()), []byte("ICC profile")) {
// Reset reader to beginning
if seeker, ok := wrappedReader.(io.Seeker); ok {
_, _ = seeker.Seek(0, io.SeekStart)
}
// Try again without color space conversion
img, err = imaging.Decode(wrappedReader, imaging.ColorSpace(imaging.NO_CHANGE_OF_COLORSPACE))
if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}
} else {
return fmt.Errorf("failed to decode image: %w", err)
}
return fmt.Errorf("failed to decode image: %w", err)
}
// Note: For HEIC files processed via FFmpeg, orientation is handled automatically
+6 -2
View File
@@ -276,7 +276,9 @@ func TestService_Resize(t *testing.T) {
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
svc := NewPreviewGenerator(1, "")
// Use a temporary directory for cache to avoid creating directories in the source tree
tmpDir := t.TempDir()
svc := NewPreviewGenerator(1, tmpDir)
source := test.source(t)
defer source.Close()
@@ -434,7 +436,9 @@ func TestService_FormatFromExtension(t *testing.T) {
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
svc := NewPreviewGenerator(1, "")
// Use a temporary directory for cache to avoid creating directories in the source tree
tmpDir := t.TempDir()
svc := NewPreviewGenerator(1, tmpDir)
got, err := svc.FormatFromExtension(test.ext)
require.Truef(t, errors.Is(err, test.wantErr), "error = %v, wantErr %v", err, test.wantErr)
if err != nil {
+15 -7
View File
@@ -30,6 +30,7 @@ var (
type Service struct {
fileCache diskcache.Interface
cacheDir string // Cache directory used for thumbnails and temp files
debug bool
docGenMutex sync.Mutex // Mutex to serialize access to doc generation
docSemaphore chan struct{} // Semaphore for document generation
@@ -44,36 +45,43 @@ func NewPreviewGenerator(concurrencyLimit int, cacheDir string) *Service {
}
// get round up half value of concurrencyLimit
ffmpegConcurrencyLimit := (concurrencyLimit + 1) / 2
actualCacheDir := cacheDir
if actualCacheDir == "" {
actualCacheDir = os.TempDir()
}
var fileCache diskcache.Interface
// Use file cache if cacheDir is specified
var err error
fileCache, err = diskcache.NewFileCache(cacheDir)
fileCache, err = diskcache.NewFileCache(actualCacheDir)
if err != nil {
logger.Error("The cache dir could not be created. Make sure the user that you executed the program with has access to create directories in the local path. See ")
logger.Fatalf("failed to create file cache path, which is now require to run the server: %v", err)
}
// Create directories recursively
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs"), fileutils.PermDir)
// Create directories recursively using the determined cache directory
err = os.MkdirAll(filepath.Join(actualCacheDir, "thumbnails", "docs"), fileutils.PermDir)
if err != nil {
logger.Error(err)
}
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos"), fileutils.PermDir)
err = os.MkdirAll(filepath.Join(actualCacheDir, "thumbnails", "videos"), fileutils.PermDir)
if err != nil {
logger.Error(err)
}
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "heic"), fileutils.PermDir)
err = os.MkdirAll(filepath.Join(actualCacheDir, "heic"), fileutils.PermDir)
if err != nil {
logger.Error(err)
}
videoService := ffmpeg.NewFFmpegService(ffmpegConcurrencyLimit, settings.Config.Integrations.Media.Debug, "")
imageService := ffmpeg.NewFFmpegService(concurrencyLimit, settings.Config.Integrations.Media.Debug, filepath.Join(settings.Config.Server.CacheDir, "heic"))
imageService := ffmpeg.NewFFmpegService(concurrencyLimit, settings.Config.Integrations.Media.Debug, filepath.Join(actualCacheDir, "heic"))
settings.Env.MuPdfAvailable = docEnabled()
logger.Debugf("MuPDF Enabled : %v", settings.Env.MuPdfAvailable)
logger.Debugf("Media Enabled : %v", settings.MediaEnabled())
return &Service{
fileCache: fileCache,
cacheDir: actualCacheDir,
debug: settings.Config.Integrations.Media.Debug,
docSemaphore: make(chan struct{}, 1), // must be 1 because cgo thread limit
officeSem: make(chan struct{}, concurrencyLimit),
@@ -179,7 +187,7 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
convertHEIC := strings.HasPrefix(file.Type, "image/heic") && settings.Config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview]
// Generate an image from office document
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
tempFilePath := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs", hash) + ".txt"
tempFilePath := filepath.Join(service.cacheDir, "thumbnails", "docs", hash) + ".txt"
imageBytes, err = service.GenerateImageFromDoc(ctx, file, tempFilePath, 0) // 0 for the first page
if err != nil {
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
+16 -1
View File
@@ -2917,6 +2917,9 @@ const docTemplate = `{
"hash": {
"type": "string"
},
"shareUrl": {
"type": "string"
},
"status": {
"type": "string"
},
@@ -3063,6 +3066,9 @@ const docTemplate = `{
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -3844,7 +3850,7 @@ const docTemplate = `{
"type": "string"
},
"cacheDirCleanup": {
"description": "whether to automatically cleanup the cache directory (default: true)",
"description": "whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)",
"type": "boolean"
},
"database": {
@@ -4348,6 +4354,9 @@ const docTemplate = `{
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -4478,6 +4487,9 @@ const docTemplate = `{
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -4614,6 +4626,9 @@ const docTemplate = `{
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
+16 -1
View File
@@ -2906,6 +2906,9 @@
"hash": {
"type": "string"
},
"shareUrl": {
"type": "string"
},
"status": {
"type": "string"
},
@@ -3052,6 +3055,9 @@
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -3833,7 +3839,7 @@
"type": "string"
},
"cacheDirCleanup": {
"description": "whether to automatically cleanup the cache directory (default: true)",
"description": "whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)",
"type": "boolean"
},
"database": {
@@ -4337,6 +4343,9 @@
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -4467,6 +4476,9 @@
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
@@ -4603,6 +4615,9 @@
"description": "type of share: normal, upload, max",
"type": "string"
},
"shareURL": {
"type": "string"
},
"sidebarLinks": {
"description": "customizable sidebar links",
"type": "array",
+12 -1
View File
@@ -16,6 +16,8 @@ definitions:
properties:
hash:
type: string
shareUrl:
type: string
status:
type: string
url:
@@ -115,6 +117,8 @@ definitions:
shareType:
description: 'type of share: normal, upload, max'
type: string
shareURL:
type: string
sidebarLinks:
description: customizable sidebar links
items:
@@ -695,7 +699,8 @@ definitions:
files
type: string
cacheDirCleanup:
description: 'whether to automatically cleanup the cache directory (default:
description: 'whether to automatically cleanup the cache directory. Note:
docker must also mount a persistent volume to persist the cache (default:
true)'
type: boolean
database:
@@ -1073,6 +1078,8 @@ definitions:
shareType:
description: 'type of share: normal, upload, max'
type: string
shareURL:
type: string
sidebarLinks:
description: customizable sidebar links
items:
@@ -1164,6 +1171,8 @@ definitions:
shareType:
description: 'type of share: normal, upload, max'
type: string
shareURL:
type: string
sidebarLinks:
description: customizable sidebar links
items:
@@ -1259,6 +1268,8 @@ definitions:
shareType:
description: 'type of share: normal, upload, max'
type: string
shareURL:
type: string
sidebarLinks:
description: customizable sidebar links
items:
+1 -1
View File
@@ -49,7 +49,7 @@ server:
externalUrl: "" # used by share links if set (eg. http://mydomain.com)
internalUrl: "" # used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080)
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
cacheDirCleanup: true # whether to automatically cleanup the cache directory (default: true)
cacheDirCleanup: true # whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)
maxArchiveSize: 50 # max pre-archive combined size of files/folder that are allowed to be archived (in GB)
filesystem: # filesystem settings
createFilePermission: "644" # Unix permissions like 644, 755, 2755 (default: 644) validate:required,file_permission
+45 -5
View File
@@ -81,7 +81,7 @@ export async function put(source, path, content = '') {
return await resourceAction(source, path, 'PUT', content)
}
export function download(format, files, shareHash = "") {
export async function download(format, files, shareHash = "") {
if (format !== 'zip') {
format = 'tar.gz'
}
@@ -102,13 +102,53 @@ export function download(format, files, shareHash = "") {
})
const url = window.origin + apiPath
// Create a temporary <a> element to trigger the download
// Use fetch to capture errors in the console
const response = await fetch(url, {
credentials: 'same-origin',
headers: {
"sessionId": state.sessionId,
},
})
if (!response.ok) {
const errorText = await response.text()
const error = new Error(`Download failed: ${response.status} ${response.statusText}`)
console.error('Download API error:', {
status: response.status,
statusText: response.statusText,
body: errorText,
url: url
})
throw error
}
// Get the blob from the response
const blob = await response.blob()
// Determine filename from Content-Disposition header or use default
let filename = 'download'
const contentDisposition = response.headers.get('Content-Disposition')
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/['"]/g, '')
}
} else {
// Fallback: use format extension
filename = `download.${format}`
}
// Create a blob URL and trigger download
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', '') // Ensures it triggers a download
link.href = blobUrl
link.setAttribute('download', filename)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // Clean up
document.body.removeChild(link)
// Clean up the blob URL
window.URL.revokeObjectURL(blobUrl)
}
export function post(
+1 -15
View File
@@ -1,7 +1,6 @@
import { adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath, getPublicApiPath, encodedPath, doubleEncode } from "@/utils/url.js";
import { globalVars } from "@/utils/constants";
import { getPublicApiPath, encodedPath, doubleEncode } from "@/utils/url.js";
import { state } from "@/store";
// ============================================================================
@@ -92,19 +91,6 @@ export function getPreviewURL(path,size="small") {
}
}
// Get the shareable URL for a share
/**
* @param {{ hash: string }} share
* @returns {string}
*/
export function getShareURL(share) {
if (globalVars.externalUrl) {
const apiPath = getApiPath(`public/share/${share.hash}`)
return globalVars.externalUrl + apiPath;
}
return window.origin + getApiPath(`public/share/${share.hash}`);
}
export function post(
hash,
+25 -4
View File
@@ -4,12 +4,10 @@
<div class="status-info">
<span v-if="selectedCount > 0">
<span class="button">{{ selectedCount }}</span>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ $t(selectedCount === 1 ? 'files.itemSelected' : 'files.itemsSelected') }} ({{ displayTotalSize }})
{{ selectedItemsText }}
</span>
<span v-else class="directory-info">
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ numDirs }} {{ $t(numDirs === 1 ? 'buttons.folder' : 'general.folders') }} | {{ numFiles }} {{ $t(numFiles === 1 ? 'buttons.file' : 'general.files') }} ({{ displayTotalSize }})
{{ directoryInfoText }}
</span>
</div>
<div class="status-controls">
@@ -87,6 +85,29 @@ export default {
const size = this.selectedCount > 0 ? this.totalSelectedSize : this.totalDirectorySize;
return getHumanReadableFilesize(size);
},
// i18n labels with compile-time checked keys
itemsSelectedLabel() {
return this.selectedCount === 1
? this.$t("files.itemSelected")
: this.$t("files.itemsSelected");
},
foldersLabel() {
return this.numDirs === 1
? this.$t("general.folder")
: this.$t("general.folders");
},
filesLabel() {
return this.numFiles === 1
? this.$t("general.file")
: this.$t("general.files");
},
// Complete text with raw characters moved from template
selectedItemsText() {
return `${this.itemsSelectedLabel} (${this.displayTotalSize})`;
},
directoryInfoText() {
return `${this.numDirs} ${this.foldersLabel} | ${this.numFiles} ${this.filesLabel} (${this.displayTotalSize})`;
},
},
methods: {
updateGallerySize(event) {
+19 -2
View File
@@ -78,6 +78,7 @@ import { filesApi,publicApi } from "@/api";
import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { url } from "@/utils";
import { notify } from "@/notify";
import Icon from "@/components/files/Icon.vue";
export default {
@@ -358,6 +359,7 @@ export default {
/** @param {DragEvent} event */
async drop(event) {
this.isDraggedOver = false;
if (!this.canDrop) return;
// Only allow internal drags (from filebrowser items), not external files from desktop
const isInternal = Array.from(event.dataTransfer.types).includes(
@@ -440,9 +442,24 @@ export default {
} else {
await filesApi.moveCopy(items, "move", overwrite, rename);
}
// Close the prompt after successful operation
notify.showSuccess(this.$t("prompts.moveSuccess"));
// Notification to move into the folder
const buttonAction = () => {
this.open();
};
const buttonProps = {
icon: "folder",
buttons: [
{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}
]
};
notify.showSuccess(this.$t("prompts.moveSuccess"), buttonProps);
// Close the prompt after successful operation and reload items for reflect the changes
mutations.closeHovers();
mutations.setReload(true);
} catch (error) {
// Close the prompt and let error handling continue
mutations.closeHovers();
@@ -25,9 +25,7 @@
<strong>{{ $t("prompts.size", { suffix: ":" }) }}</strong> {{ humanSize }}
<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
</div>
</div>
</div>
</div>
@@ -87,7 +87,6 @@ export default {
},
confirm() {
this.onConfirm();
mutations.setReload(true);
mutations.closeHovers();
},
},
+2 -1
View File
@@ -188,10 +188,11 @@ export default {
// Sets the current path and resets
// the current items.
this.current = req.path;
this.source = req.source;
this.source = req.source || null; // For shares, source might be null/undefined
this.items = [];
// Emit both path and source
// For shares, source will be null, which is handled by MoveCopy
this.$emit("update:selected", {
path: this.current,
source: this.source
+8 -9
View File
@@ -122,12 +122,8 @@ export default {
return this.destPath.startsWith(itemPath + "/") || this.destPath === itemPath;
},
canCreateFolder() {
// Detect correct permission for shares for show the new folder button.
if (getters.isShare()) {
return state.shareInfo?.allowCreate;
} else {
return state.user?.permissions?.modify;
}
const perms = getters.permissions();
return !!perms?.create;
},
closeHovers() {
return mutations.closeHovers();
@@ -316,14 +312,17 @@ export default {
const destPath = this.destPath;
// Show success notification with optional button to navigate to destination
// For shares, destSource might be null, but goToItem handles shares via state.shareInfo.hash
const buttonAction = () => {
if (destSource && destPath) {
goToItem(destSource, destPath, {});
if (destPath) {
// For shares, goToItem will use state.shareInfo.hash, so source can be null
// For regular files, destSource should be set
goToItem(destSource || null, destPath, {});
}
};
const buttonProps = {
icon: "folder",
buttons: destSource && destPath ? [
buttons: destPath ? [
{
label: this.$t("buttons.goToItem"),
primary: true,
+10 -10
View File
@@ -41,6 +41,7 @@
import { filesApi, publicApi } from "@/api";
import { mutations, state, getters } from "@/store";
import { notify } from "@/notify";
import { getFileExtension, removePrefix } from '@/utils/files.js'
export default {
name: "rename",
props: {
@@ -51,16 +52,14 @@ export default {
}
},
data() {
// Separate filename and extension for avoid renmame mistakenly the extension too
const itemName = this.item.name;
if (this.item.type !== 'directory') {
const lastDotIndex = this.item.name.lastIndexOf('.');
const hasExtension = lastDotIndex > 0;
const ext = getFileExtension(this.item.name);
const filenamePrefix = this.item.name.substring(0, this.item.name.length - ext.length);
return {
fileName: hasExtension ? itemName.substring(0, lastDotIndex) : itemName,
fileExtension: hasExtension ? itemName.substring(lastDotIndex + 1) : "",
name: itemName,
fileName: filenamePrefix,
fileExtension: removePrefix(ext, "."),
name: itemName, // Initialize name for non-directory items
};
}
return {
@@ -108,7 +107,8 @@ export default {
* @param {string} value
*/
validateFileName(value) {
if (value === "") {
// Handle undefined, null, or empty values
if (!value || value === "") {
return { valid: true };
}
@@ -125,7 +125,7 @@ export default {
// Check if the item already exists
for (const item of state.req.items) {
if (item.path === this.item.path) continue;
if (item.name.toLowerCase() === value.toLowerCase()) {
if (item.name && item.name.toLowerCase() === value.toLowerCase()) {
return { valid: false, reason: 'conflict' };
}
}
@@ -194,7 +194,7 @@ export default {
}
.extension-input {
width: 80px;
width: 6em;
}
.extension-separator {
+5 -33
View File
@@ -55,14 +55,14 @@
</button>
</td>
<td class="small">
<button class="action copy-clipboard" :data-clipboard-text="buildLink(link)"
<button class="action copy-clipboard" :data-clipboard-text="link.shareURL"
:aria-label="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')">
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small" v-if="hasDownloadLink()">
<td class="small">
<button :disabled="link.shareType == 'upload'" class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:data-clipboard-text="link.downloadURL" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')">
<i class="material-icons">content_paste_go</i>
</button>
@@ -307,10 +307,10 @@
<script>
import { notify } from "@/notify";
import { state, getters, mutations } from "@/store";
import { publicApi, shareApi } from "@/api";
import { shareApi } from "@/api";
import Clipboard from "clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
import { buildItemUrl } from "@/utils/url";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import SettingsItem from "@/components/settings/SettingsItem.vue";
import FileList from "./FileList.vue";
@@ -746,31 +746,6 @@ export default {
humanTime(time) {
return fromNow(time, state.user.locale)
},
/**
* @param {Share} share
*/
buildLink(share) {
return publicApi.getShareURL(share);
},
hasDownloadLink() {
// Check if we have a single selected item that can be downloaded
return !this.item?.isDir;
},
/**
* @param {Share} share
*/
buildDownloadLink(share) {
if (share.downloadURL) {
// Only fix the URL if it doesn't already have the correct external domain
if (globalVars.externalUrl) {
// URL already has the correct external domain, use as-is
return share.downloadURL;
}
// URL needs fixing (internal domain or no externalUrl set)
return this.fixDownloadURL(share.downloadURL);
}
return publicApi.getDownloadURL(share, [this.item.name]);
},
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1;
@@ -816,9 +791,6 @@ export default {
];
}
},
fixDownloadURL(downloadUrl) {
return fixDownloadURL(downloadUrl);
},
/**
* @param {{path: string, source: string}} pathOrData
*/
@@ -97,9 +97,7 @@ export default {
},
methods: {
getShareLink() {
return publicApi.getShareURL({
hash: state.share.hash,
});
return state.shareInfo.shareURL;
},
close() {
mutations.closeHovers();
+29 -3
View File
@@ -341,6 +341,21 @@ export default {
}
this.user = { ...(await usersApi.get(id)) };
this.user.password = "";
// Normalize scopes to ensure they're in {name, scope} format only
if (this.user.scopes && Array.isArray(this.user.scopes)) {
this.user.scopes = this.user.scopes.map(scope => {
// If it's already in the correct format, use it
if (scope.name !== undefined && scope.scope !== undefined) {
return { name: scope.name, scope: scope.scope || "" };
}
// If it's a full source object, extract just name and scope
if (scope.name && typeof scope.name === 'string') {
return { name: scope.name, scope: scope.scope || "" };
}
// Fallback: try to extract from any object structure
return { name: "", scope: "" };
});
}
// Ensure loginMethod is valid, set to first available method if not set or invalid
const validMethods = [];
if (this.globalVars.passwordAvailable) validMethods.push("password");
@@ -376,7 +391,11 @@ export default {
if (this.isNew && this.availableSources.length) {
const newSource = this.availableSources.shift();
if (newSource) {
this.selectedSources.push(newSource);
// Only store {name, scope} format, not the full source config
this.selectedSources.push({
name: newSource.name || "",
scope: "" // Empty scope - backend will handle defaults
});
this.emitUserUpdate();
}
}
@@ -388,18 +407,25 @@ export default {
event.preventDefault();
try {
let fields = ["all"];
// Transform selectedSources to only include {name, scope} format
// Empty scope strings should be passed as "" for backend to handle defaults
const scopesToSend = this.selectedSources.map(source => ({
name: source.name || "",
scope: source.scope || ""
}));
if (this.isNew) {
if (!state.user.permissions.admin) {
notify.showError(this.$t("settings.userNotAdmin"));
return;
}
await usersApi.create({ ...this.user, scopes: this.selectedSources });
await usersApi.create({ ...this.user, scopes: scopesToSend });
// Emit event to refresh user list
eventBus.emit('usersChanged');
// Close the modal
mutations.closeHovers();
} else {
await usersApi.update({ ...this.user, scopes: this.selectedSources }, fields);
await usersApi.update({ ...this.user, scopes: scopesToSend }, fields);
// Only emit usersChanged for admin user management, not profile updates
if (state.user.permissions.admin && this.user.id !== state.user.id) {
eventBus.emit('usersChanged');
+2 -1
View File
@@ -274,7 +274,8 @@ body.rtl #listingView {
#listingView .header .duration,
#listingView.list .listing-item .duration,
#listingView.compact .listing-item .duration {
margin: auto;
margin-left: auto;
padding-right: 1em;
}
#listingView.list .listing-item .size.adjustment,
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "تنزيل الملفات"
},
"permissions-name": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
"profileSettings": "إعدادات الحساب",
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "الوكيل"
},
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي"
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي",
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام."
},
"sidebar": {
"siteSettings": "إعدادات الموقع",
+2 -2
View File
@@ -426,7 +426,6 @@
"download": "Soubory ke stažení"
},
"permissions-name": "Oprávnění",
"permissionsHelp": "Uživatele můžete nastavit jako administrátora nebo vybrat jednotlivá oprávnění. Pokud zvolíte \"Administrátor\", ostatní možnosti budou automaticky zaškrtnuty. Správa uživatelů zůstává pouze výsadou administrátora.\n",
"profileSettings": "Nastavení profilu",
"ruleExample1": "zamezí přístup ke všem skrytým souborům (např. .git, .gitignore) ve všech složkách.\n",
"ruleExample2": "blokuje přístup k souboru s názvem Caddyfile v kořeni rozsahu.",
@@ -454,7 +453,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "např. '/subfolder' - prázdný pro výchozí nastavení"
"newUserHintSubFolder": "např. '/subfolder' - prázdný pro výchozí nastavení",
"permissionsHelp": "Oprávnění uživatele řídí přístup k typům akcí, které může uživatel provádět. Přístup administrátora poskytuje plnou kontrolu nad aktualizací a změnou nastavení libovolného uživatele i nad mnoha nastaveními systému."
},
"sidebar": {
"siteSettings": "Nastavení webu",
+2 -2
View File
@@ -276,7 +276,6 @@
"download": "Dateien herunterladen"
},
"permissions-name": "Berechtigungen",
"permissionsHelp": "Sie können einem Benutzer Administratorrechte einräumen oder die Berechtigunen individuell festlegen. Wenn Sie \"Administrator\" auswählen, werden alle anderen Rechte automatisch vergeben. Die Nutzerverwaltung kann nur durch einen Administrator erfolgen.\n",
"profileSettings": "Darstellung",
"ruleExample1": "Verhindert den Zugang zu versteckten Dateien (dot-Files, wie .git, .gitignore) in allen Ordnern\n",
"ruleExample2": "Blockiert den Zugang auf Dateien mit dem Namen Caddyfile in der Wurzel/Basis des Scopes.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard"
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard",
"permissionsHelp": "Die Benutzerrechte steuern den Zugriff auf die Art der Aktionen, die der Benutzer durchführen kann. Der Administrator-Zugang gibt Ihnen die volle Kontrolle über die Aktualisierung und Änderung der Benutzereinstellungen sowie vieler Systemeinstellungen."
},
"sidebar": {
"siteSettings": "Seiteneinstellungen",
+2 -2
View File
@@ -282,7 +282,6 @@
"download": "Λήψη αρχείων"
},
"permissions-name": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
@@ -326,7 +325,8 @@
"oidc": "OIDC",
"proxy": "Πληρεξούσιος"
},
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή"
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή",
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος."
},
"sidebar": {
"siteSettings": "Ρυθμίσεις ιστότοπου",
+1 -1
View File
@@ -550,7 +550,7 @@
"download": "Download files"
},
"permissions-name": "Permissions",
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
"permissionsHelp": "User permissions control access to what type of actions the user can perform. Administrar access gives full control to update and change any user's settings as well as many system settings.",
"profileSettings": "Profile Settings",
"ruleExample1": "prevents the access to any dot file (such as .git, .gitignore) in every folder.\n",
"ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
+207 -207
View File
@@ -3,7 +3,7 @@
"copyFile": "Copiar archivo",
"copyToClipboard": "Copiar al portapapeles",
"moveFile": "Mover archivo",
"permalink": "Link sin expiración",
"permalink": "Enlace permanente",
"reportIssue": "Reportar problema",
"selectMultiple": "Selección múltiple",
"shell": "Presiona Enter para buscar...",
@@ -30,7 +30,7 @@
"saveAndExit": "Guardar y salir",
"keepEditing": "Seguir editando",
"closeAnyway": "Cerrar de todos modos",
"goToItem": "Ir al artículo"
"goToItem": "Ir al elemento"
},
"download": {
"downloadFile": "Descargar archivo",
@@ -42,8 +42,8 @@
"internal": "Error interno :(",
"notFound": "No se encontró el directorio",
"connection": "No se puede conectar al servidor.",
"shareNotFound": "No se encuentra lo que buscas, tal vez el enlace expiró.",
"failedToConnectToServer": "No se ha podido conectar con el servidor, ¿sigue funcionando?"
"shareNotFound": "No se encuentró lo que buscas, tal vez el enlace expiró.",
"failedToConnectToServer": "No se ha podido conectar con el servidor, ¿Sigue funcionando?"
},
"files": {
"closePreview": "Cerrar vista previa",
@@ -60,7 +60,7 @@
"myFiles": "Mis archivos{suffix}",
"newFile": "Nuevo archivo{suffix}",
"newFolder": "Nueva carpeta{suffix}",
"duration": "Duración{suffix}",
"duration": "Duración {suffix}",
"sortByDuration": "Ordenar por duración"
},
"help": {
@@ -71,7 +71,7 @@
"ctrl": {
"click": "Selecciona varios elementos (Selección multiple)",
"d": "Descargar elementos seleccionados",
"b": "Mostrar/ocultar la barra lateral",
"b": "Mostrar/ocultar el panel lateral",
"comma": "Ir a configuración",
"c": "Copiar elementos seleccionados",
"v": "Pegar elementos copiados o cortados",
@@ -81,10 +81,10 @@
"esc": "Cancela la selección o cierra la ventana actual",
"f2": "Cambiar nombre del archivo o carpeta",
"f1": "Abrir ventana de ayuda",
"description": "Aquí puedes ver todos los atajos de teclado básicos. Si quieres saber más, visita la",
"slash": "Activar búsqueda",
"description": "Aquí puedes ver todos los atajos de teclado básicos. Para saber más, visita la",
"slash": "Abrir panel de búsqueda",
"wiki": "documentación oficial de FileBrowser Quantum",
"marqueeSelect": "seleccionar varios archivos o directorios arrastrando sobre el espacio vacío"
"marqueeSelect": "Selecciona archivos o carpetas con el mouse arrastrando en un espacio vacio"
},
"languages": {
"he": "עברית",
@@ -139,17 +139,17 @@
"moveMessage": "Selecciona un nuevo destino:",
"newArchetype": "Crear una nueva publicación en base a una plantilla, tu archivo se creara en la misma carpeta",
"newDir": "Nueva carpeta",
"newDirMessage": "Nombre de la carpeta",
"newDirMessage": "Nombre de la carpeta:",
"newFile": "Nuevo archivo",
"newFileMessage": "Nombre del archivo",
"newFileMessage": "Nombre del archivo:",
"rename": "Cambiar nombre",
"replace": "Reemplazar",
"replaceMessage": "Un archivo con el mismo nombre ya existe. ¿Quieres cambiar el nombre?\n",
"schedule": "Programar",
"scheduleMessage": "Selecciona una fecha y hora para la publicacion:",
"show": "Mostrar",
"upload": "Subir",
"optionalPassword": "Contraseña (opcional)",
"upload": "Subir Archivos",
"optionalPassword": "Contraseña: (opcional)",
"deleteUserMessage": "¿Estas seguro que quieres eliminar este usuario?",
"filesSelected": "Archivos seleccionados",
"typeName": "Tipo:",
@@ -168,7 +168,7 @@
"optionalPasswordHelp": "Si eliges una contraseña, los usuarios deberán introducir esa misma contraseña para ver el contenido.",
"dragAndDrop": "Arrastra y suelta los archivos aquí",
"completed": "Completado",
"conflictsDetected": "Conflictos detectados",
"conflictsDetected": "Se detectaron conflictos",
"viewMode": "Cambiar modo de vista",
"renameMessage": "Introduce un nuevo nombre:",
"renameMessageInvalid": "El archivo no puede tener \"/\" o \"\\\"",
@@ -176,10 +176,10 @@
"deleteMessageShare": "¿Estás seguro que quieres borrar \"{path}\" de los recursos compartidos?",
"numberDirs": "Número de carpetas {suffix}",
"numberFiles": "Número de archivos {suffix}",
"renameMessageConflict": "Ya existe un archivo llamado \"{filename}\".",
"renameMessageConflict": "Ya existe un archivo llamado \"{filename}\"",
"uploadSettingsChunked": "Subidas simultáneas: {maxConcurrentUpload} - Tamaño de fragmento: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Subidas simultáneas: {maxConcurrentUpload} - Subida fragmentada: Desactivada",
"quickJump": "Salta rápidamente a otros archivos",
"quickJump": "Navegación rápida",
"moveSuccess": "¡Archivos movidos con éxito!",
"copySuccess": "¡Archivos copiados correctamente!",
"lastModified": "Última modificación: {suffix}",
@@ -192,37 +192,37 @@
"moveItemQuestion": "¿Quieres mover este archivo a la nueva ubicación?",
"sourcePath": "Fuente:",
"destinationPath": "Nueva ubicación:",
"closeWithActiveUploadsMessage": "Si cierras este aviso, todas las subidas activas se interrumpirán, y puede que algunos archivos queden incompletos. ¿Que quieres hacer?",
"closeWithActiveUploadsMessage": "Si cierras esta ventana, todos los archivos que estés subiendo se interrumpirán, y puede que algunos queden incompletos. ¿Que quieres hacer?",
"closeWithActiveUploads": "Subida de archivos activa",
"basicInfo": "Información básica",
"directoryInfo": "Información del directorio",
"additionalInfo": "Información adicional",
"hidden": "Oculto",
"hasPreview": "Tiene Vista Previa",
"hasPreview": "Vista Previa",
"checksums": "Sumas de control",
"hashAlgorithm": "Algoritmo Hash",
"hashAlgorithm": "Algoritmo hash",
"hashValue": "Valor Hash",
"selectHashAlgorithm": "Seleccione un algoritmo hash",
"generate": "Genere",
"errorGeneratingHash": "Error al generar el hash",
"hashCopied": "Hash copiado en el portapapeles",
"errorCopyingHash": "Error al copiar el hash",
"token": "Ficha",
"selectHashAlgorithm": "Seleccione un algoritmo",
"generate": "Generar",
"errorGeneratingHash": "Hubo un error al generar 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!",
"halloweenTitle": "¡Feliz Halloween!",
"halloweenBody": "Se ha aplicado el tema de Halloween. Puedes desactivarlo si no te gusta, o desactivarlo permanentemente en los ajustes del perfil seleccionando cualquier otro tema.",
"firstLoadBody": "Se ha creado una nueva base de datos al iniciar. Si esto no es lo que esperaba, por favor, compruebe su archivo de configuración para la ruta `server.database` y asegúrese de que existe. En docker, la ruta de la base de datos puede ser `./data/database.db` o `./database.db`.",
"firstLoadBody": "Se acabó de crear una nueva base de datos al iniciar. Si esto no es lo que esperabas, por favor, verifica si tu archivo de configuración tiene una entrada para la ruta de la base de datos: `server.database` y aseguráte de que existe. Si usas Docker, la ruta de la base de datos puede ser `./data/database.db` o `./database.db`.",
"selectPath": "Seleccionar ruta",
"renameSuccess": "El cambio de nombre se ha realizado correctamente.",
"newFileSuccess": "¡Fichero creado con éxito!",
"newDirSuccess": "¡Directorio creado con éxito!",
"copyToClipboardFailed": "No se puede copiar en el portapapeles"
"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"
},
"search": {
"smallerThan": "Menor que:",
"largerThan": "Mayor que:",
"helpText1": "La busqueda se realiza por cada letra introducida (empezando de 3)",
"helpText1": "La busqueda se realiza por cada letra introducida (empezando de {minSearchLength})",
"helpText2": "El índice se actualiza automáticamente cada 5 min por defecto. Si se interrumpe pueden haber errores",
"onlyFolders": "Solo carpetas",
"onlyFiles": "Solo archivos",
@@ -239,7 +239,7 @@
"allowEdit": "Editar, borrar y cambiar nombres de archivos o carpetas",
"allowNew": "Crear nuevos archivos y carpetas",
"allowPublish": "Publicar posts/páginas",
"allowSignup": "Permitir registro de nuevos usuarios",
"allowSignup": "Permitir el registro de nuevos usuarios",
"avoidChanges": "(dejar en blanco para evitar cambios)",
"brandingDirectoryPath": "Ruta del directorio con los archivos que usarás para personalizar FileBrowser Quantum",
"changePassword": "Cambiar contraseña",
@@ -277,17 +277,16 @@
"download": "Descargar archivos"
},
"permissions-name": "Permisos",
"permissionsHelp": "Puedes elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las demás opciones serán activadas, y los usuarios administradores podrán modificar a otros usuarios.\n",
"profileSettings": "Ajustes del perfil",
"ruleExample1": "Bloquea el acceso a una extensión especifica extensión de archivo (ej: .git) en todas las carpetas.\n",
"ruleExample2": "Bloquea el acceso al archivo llamado \"Caddyfile\" en la carpeta raíz (/)",
"rulesHelp": "Puedes definir un conjunto de reglas para este usuario. Los archivos bloqueados no se mostrarán y no serán accesibles pora el usuario. Puedes utilizar regex y rutas relativas al alcance (Ruta base) del usuario.\n",
"settingsUpdated": "¡Ajustes actualizados!",
"shareManagement": "Gestión de recursos compartidos",
"shareDeleted": "¡Compartido eliminado!",
"shareDeleted": "¡Recurso compartido eliminado!",
"singleClick": "Abrir con un solo clic",
"userCommandsHelp": "Comandos permitidos (separados por espacios). Ejemplo:\n",
"userCreated": "¡Usuario creado!",
"userCreated": "¡Usuario creado con éxito!",
"userDefaults": "Configuración de usuario por defecto",
"userDeleted": "¡Usuario eliminado!",
"userManagement": "Gestión de usuarios",
@@ -299,8 +298,8 @@
"emptyGlobal": "Actualmente no hay configuracion global, edita el archivo YAML para establecer las configuraciones del sistema",
"UserManagement": "Gestión de usuarios",
"tusUploads": "Subida fragmentada",
"tusUploadsHelp": "File Browser Quantum permite subir archivos en fragmentos, lo que permite que sean subidos de manera eficiente, fiable, y que sea reanudable incluso si tu internet es inestable",
"tusUploadsChunkSize": "Tamaño máximo del \"fragmento\" del archivo a subir. Puedes escribir un número en bytes o una cadena como 10 MB, 1 GB, etc",
"tusUploadsHelp": "FileBrowser Quantum permite subir archivos en fragmentos, lo que permite que sean subidos de manera eficiente, fiable, y que sea reanudable incluso si tu internet es inestable",
"tusUploadsChunkSize": "Tamaño máximo del fragmento del archivo a subir. Puedes escribir un número en bytes o una cadena como 10 MB, 1 GB, etc",
"tusUploadsRetryCount": "Número de reintentos. Por si falla la subida de algún archivo",
"api": "Claves API",
"loginMethodDescription": "Método de inicio de sesión",
@@ -323,45 +322,46 @@
"configViewerShowComments": "Mostrar comentarios",
"configViewerLoadConfig": "Cargar configuración",
"brandingHelp": "Puedes personalizar la apariencia de FileBrowser Quantum cambiando el nombre, el logo, puedes añadir tus propios estilos personalizados, desactivar los enlaces externos a GitHub, etc. \nSi quieres saber más, visita {0}.",
"commandRunnerHelp": "Aquí puede establecer los comandos que se ejecutan en los eventos nombrados. Debe escribir uno por línea. Las variables de entorno {0} y {1} estarán disponibles, siendo {0} relativa a {1}. Para más información sobre esta característica y las variables de entorno disponibles, por favor lea el {2}."
"commandRunnerHelp": "Aquí puede establecer los comandos que se ejecutan en los eventos nombrados. Debe escribir uno por línea. Las variables de entorno {0} y {1} estarán disponibles, siendo {0} relativa a {1}. Para más información sobre esta característica y las variables de entorno disponibles, por favor lea el {2}.",
"permissionsHelp": "Los permisos de usuario controlan el acceso al tipo de acciones que el usuario puede realizar. El acceso de administrador proporciona un control total para actualizar y cambiar la configuración de cualquier usuario, así como muchos ajustes del sistema."
},
"sidebar": {
"siteSettings": "Ajustes del sitio",
"updateIsAvailable": "¡Actualización disponible!",
"fileActions": "Acciones de archivo",
"customizeLinks": "Personalizar los enlaces de la barra lateral",
"customizeLinksDescription": "Personaliza los enlaces que aparecen en tu barra lateral. Arrastre y suelte para reordenar.",
"currentLinks": "Enlaces actuales",
"addNewLink": "Añadir nuevo enlace",
"linkType": "Tipo de enlace",
"selectLinkType": "Seleccione un tipo de enlace",
"customLink": "Enlace personalizado",
"selectSource": "Seleccionar fuente",
"chooseSource": "Elija una fuente",
"selectTool": "Seleccionar herramienta",
"chooseTool": "Elija una herramienta",
"linkName": "Nombre del enlace",
"linkNamePlaceholder": "Introduzca el nombre del enlace",
"linkUrl": "Enlace URL",
"linkUrlPlaceholder": "Introduzca la URL o la ruta (por ejemplo, /archivos/ruta o https://example.com)",
"linkIcon": "Icono de enlace",
"linksUpdatedSuccess": "Los enlaces de la barra lateral se han actualizado correctamente.",
"linksUpdateFailed": "Error al actualizar los enlaces de la barra lateral",
"sourceNotAccessible": "Ya no puede acceder a esta fuente",
"noLinksYet": "Aún no hay enlaces configurados. Añada su primer enlace a continuación.",
"customizeShareLinks": "Personalizar los enlaces compartidos de la barra lateral",
"customizeShareLinksDescription": "Personalice los enlaces que aparecen en la barra lateral para esta acción. Arrastre y suelte para reordenar.",
"editLink": "Editar enlace",
"selectShare": "Seleccione Compartir",
"chooseShare": "Elija una acción",
"selectSharePath": "Ruta dentro del recurso compartido (opcional)",
"sharePathPlaceholder": "por ejemplo, /subcarpeta o dejar vacío para root",
"browseIcons": "Iconos de navegación",
"customizeLinks": "Personalizar accesos directos",
"customizeLinksDescription": "Personaliza los accesos directos que aparecen en el panel lateral - Puedes arrastrar y soltar para reordenarlos",
"currentLinks": "Accesos directos actuales",
"addNewLink": "Crear acceso directo",
"linkType": "Tipo:",
"selectLinkType": "Selecciona un tipo de acceso directo",
"customLink": "Acceso directo personalizado",
"selectSource": "Seleccionar Fuente",
"chooseSource": "Elije una fuente de archivos",
"selectTool": "Selecciona una herramienta",
"chooseTool": "Elije una herramienta",
"linkName": "Nombre del acceso directo",
"linkNamePlaceholder": "Introduzca el nombre del acceso directo",
"linkUrl": "Enlace",
"linkUrlPlaceholder": "Introduzca una ruta o URL (ej: /archivos/ruta o https://example.com)",
"linkIcon": "Icono del acceso directo:",
"linksUpdatedSuccess": "¡Se actualizaron los accesos directos!",
"linksUpdateFailed": "Hubo un error al actualizar los accesos directos",
"sourceNotAccessible": "No se pudo acceder a la fuente",
"noLinksYet": "Aún no configuraste ningún acceso directo, ¡Añade uno!",
"customizeShareLinks": "Accesos directos",
"customizeShareLinksDescription": "Aquí puedes personalizar los accesos directos del panel lateral para este recurso compartido.",
"editLink": "Editar acceso directo",
"selectShare": "Seleccionar recurso compartido",
"chooseShare": "Seleccionar recurso compartido",
"selectSharePath": "Ruta que se usará dentro del recurso compartido (opcional)",
"sharePathPlaceholder": "ej: /subcarpeta o en blanco para \"/\"",
"browseIcons": "Material Icons",
"pickIcon": "Elija un icono",
"pickIconDescription": "Busque y seleccione un icono de la biblioteca Material Icons",
"shareLinksUpdatedSuccess": "Los enlaces de la barra lateral compartida se han actualizado correctamente.",
"shareHome": "Compartir",
"linkIconPlaceholder": "Opcionalmente, especifique un nombre de icono de material"
"pickIconDescription": "Aquí puedes explorar los iconos más populates de la biblioteca \"Material Icons\"",
"shareLinksUpdatedSuccess": "¡Se actualizaron los accesos directos del recurso compartido!",
"shareHome": "Recurso compartido",
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste"
},
"success": {
"linkCopied": "¡Enlace copiado!"
@@ -374,16 +374,16 @@
"unit": "Unidad"
},
"upload": {
"abortUpload": "¿Cancelar subida?"
"abortUpload": "¿Cancelar subida de archivos?"
},
"share": {
"notice": "Nota: Cuando se crea un recurso compartido, este queda más expuesto al público y tendrá diferentes normas de acceso. Si el recurso compartido no está configurado de forma segura, cualquiera podrá acceder a él.",
"disableAnonymousDescription": "Cuando está activado, sólo los usuarios registrados podrán acceder al recurso compartido.",
"disableThumbnailsDescription": "Las miniaturas de archivos, fotos y videos no estarán activadas.",
"enableAllowedUsernamesDescription": "Cuando se activa, sólo los usuarios que especifique pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.",
"downloadsLimitDescription": "Número máximo de veces que se puede descargar un archivo/carpeta del recurso compartido - Dejar vacío para ilimitado.",
"maxBandwidthDescription": "El ancho de banda máximo de descarga en kilobytes por segundo - Dejar vacío para ilimitado.",
"shareThemeDescription": "El tema que se utilizará para los usuarios que accedan al recurso compartidos",
"notice": "Cuando se crea un recurso compartido, este quedará más expuesto al público y tendrá diferentes reglas de acceso. Si no está configurado de forma forma segura, cualquiera podrá acceder a él.",
"disableAnonymousDescription": "Cuando está activado, solo los usuarios registrados podrán acceder al recurso compartido.",
"disableThumbnailsDescription": "Las miniaturas de archivos, fotos, audios y videos no estarán activadas.",
"enableAllowedUsernamesDescription": "Cuando está activado, sólo usuarios especificados podrán acceder al recurso compartido. El usuario deberá tener acceso al origen del recurso compartido para poder entrar",
"downloadsLimitDescription": "Número máximo de veces que se puede descargar un archivo/carpeta del recurso compartido - Dejar en blanco para ilimitado.",
"maxBandwidthDescription": "Velocidad máxima de descarga en kilobytes por segundo (kb/s) - Dejar en blanco para ilimitado.",
"shareThemeDescription": "El tema que se usará para los usuarios que accedan al recurso compartidos",
"disableAnonymous": "Desactivar el acceso anónimo",
"disableThumbnails": "Desactivar miniaturas",
"enableAllowedUsernames": "Compartir sólo con usuarios registrados",
@@ -404,25 +404,25 @@
"hideNavButtonsDescription": "Oculta los botones de navegación en la parte superior, es útil si quieres crear un aspecto minimalista.",
"viewModeDescription": "Diseño por defecto de la página compartida.",
"descriptionDefault": "Han compartido esto contigo.",
"disableShareCard": "Desactivar código QR",
"disableShareCardDescription": "Desactiva el código QR en el panel lateral o en la parte superior de la página en móvil",
"disableShareCard": "Desactivar tarjeta de compartir",
"disableShareCardDescription": "Desactiva la tarjeta de compartir y el código QR en el panel lateral o en la parte superior de la página en móvil",
"disableSidebar": "Desactivar el panel lateral",
"disableSidebarDescription": "Desactivar el panel lateral en la página compartida.",
"disableFileViewer": "Desactivar visor de archivos",
"enforceDarkLightMode": "Tema",
"enforceDarkLightModeDescription": "Forzar un modo de tema específico (oscuro o claro) para el recurso compartido, anulando las preferencias del usuario.",
"enforceDarkLightModeDescription": "Forzar un tema (claro u oscuro) para el recurso compartido, anulando las preferencias del usuario - Ocultará el botón de cambio de tema",
"default": "Por defecto",
"dark": "Oscuro",
"light": "Claro",
"enableOnlyOffice": "Activar el visor de OnlyOffice",
"enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
"enableOnlyOfficeDescription": "Permitir la visualización de archivos Office usando OnlyOffice en este recurso compartido.",
"titleDefault": "{title}",
"defaultViewMode": "Modo de visualización",
"defaultViewModeDescription": "Establece el modo de visualización predeterminado para los usuarios anónimos que vean esta acción. Los usuarios registrados pueden cambiar su preferencia de visualización.",
"shareType": "Tipo de compartido",
"defaultViewMode": "Modo de vista",
"defaultViewModeDescription": "Establece el modo de vista predeterminada para los que accedan a este recurso compartido. Los usuarios que estén registrados podrán cambiar su modo de vista si ellos quieren",
"shareType": "Tipo de recurso compartido:",
"shareTypeDescription": "El tipo de recurso compartido que se va a crear. Los recursos compartidos normales permiten ver y descargar archivos. Los recursos compartidos de subida permiten subir archivos al recurso compartido.",
"normalShare": "Normal",
"uploadShare": "Solo subidas",
"uploadShare": "Solo subir archivos",
"perUserDownloadLimit": "Límitar descargas para cada usuario",
"perUserDownloadLimitDescription": "Cuando se activa, el límite de descargas se aplicará a cada usuario. Los usuarios anónimos no podrán descargar ningún archivo de este recurso compartido.",
"descriptionUploadDefault": "Te han compartido un enlace para que subas archivos",
@@ -431,18 +431,18 @@
"disableDownload": "Desactivar la descarga de archivos",
"disableDownloadDescription": "Nadie podrá descargar archivos ni ver su contenido.",
"allowModify": "Permitir la edición de archivos",
"allowModifyDescription": "Cualquiera puede editar archivos en este recurso compartido.",
"allowModifyDescription": "Cualquiera podrá editar archivos en este recurso compartido.",
"allowDelete": "Permitir la eliminación de archivos",
"allowDeleteDescription": "Cualquiera puede borrar archivos y carpetas de este recurso compartido.",
"allowDeleteDescription": "Cualquiera podrá borrar archivos y carpetas de este recurso compartido.",
"allowReplacements": "Permitir la sustitución de archivos",
"allowReplacementsDescription": "Cualquiera podrá reemplazar archivos existentes al subir archivos con el mismo nombre.",
"allowReplacementsDescription": "Cualquiera podrá reemplazar archivos existentes al subir/crear archivos con el mismo nombre.",
"allowCreate": "Permitir crear/subir archivos y carpetas",
"allowCreateDescription": "Cualquiera puede crear/subir nuevos archivos y carpetas en este recurso compartido.",
"customizeSidebarLinks": "Personalizar los enlaces de la barra lateral",
"customizeSidebarLinksButton": "Personalizar enlaces",
"shareInfo": "Compartir código QR e información",
"shareInfoDescription": "Este enlace muestra la información de la acción, incluido el título, la descripción, el banner y el código QR en una ventana emergente.",
"downloadDescription": "Este enlace permite descargar todo el contenido compartido como archivo comprimido."
"allowCreateDescription": "Cualquiera podrá crear/subir nuevos archivos y carpetas en este recurso compartido.",
"customizeSidebarLinks": "Personalizar los accesos directos del panel lateral",
"customizeSidebarLinksButton": "Personalizar accesos directos",
"shareInfo": "Código QR e información del archivo",
"shareInfoDescription": "Al abrir este acceso directo, mostrará la información del recurso compartido junto con un código QR",
"downloadDescription": "Este acceso directo permite descargar todo el recurso compartido como archivo comprimido."
},
"api": {
"title": "Gestión de claves API",
@@ -462,12 +462,12 @@
"expires": "Expira el:",
"actions": "Acciones",
"enabled": "Habilitado",
"disabled": "Deshabilitado",
"permissionNote": "Elije al menos un permiso - Tu usuario también debe tener ese permiso.",
"disabled": "Desactivado",
"permissionNote": "Debes de elegir al menos un permiso - Tu usuario también debe tenerlo.",
"createKeyFailed": "Hubo un error al crear la API",
"createKeySuccess": "¡Clave API creada con exito!",
"description": "Las claves API solo podrán tener los permisos que tu usuario tenga. Para más info mira la",
"apiKeyDeleted": "Clave API eliminada"
"apiKeyDeleted": "Clave API borrada"
},
"index": {
"assessment": "Tipo",
@@ -478,34 +478,34 @@
"toggleClick": "Abrir con un solo clic",
"toggleDark": "Activar modo oscuro",
"toggleSticky": "Cambiar visibilidad del panel lateral",
"noSources": "No hay fuentes disponibles. Añada una o active \"defaultEnabled: true\" en el archivo de configuración.",
"noSources": "No se encontró ninguna fuente. Añade una, o usa \"defaultEnabled: true\" en el archivo de configuración.",
"unknown": "Desconocido",
"simple": "Simple",
"normal": "Normal",
"complex": "Complejo",
"highlyComplex": "Alta complejidad",
"highlyComplex": "Muy complejo",
"ready": "Listo",
"indexing": "Indexación",
"indexing": "Escaneando",
"unavailable": "No disponible",
"error": "Error"
},
"otp": {
"name": "Autenticación de dos factores (2FA)",
"verifyInstructions": "Ingresa el código de autenticación",
"generate": "Escanear el código QR con la app de autenticación",
"generate": "Escanear el código QR con tú app de autenticación",
"codeInputPlaceholder": "Código de 6 dígitos",
"invalidCodeType": "Código inválido: debe ser de 6 dígitos",
"invalidCodeType": "Código inválido",
"enabledSuccessfully": "¡2FA activado!",
"verificationFailed": "Verificación fallida",
"verificationSucceed": "Verificación exitosa",
"generationFailed": "La generación del código falló"
"verificationFailed": "No se pudo autenticar",
"verificationSucceed": "¡Verificación exitosa!",
"generationFailed": "Hubo un error al generar el código"
},
"general": {
"name": "Nombre",
"path": "Ruta",
"path": "Ruta:",
"user": "Usuario",
"group": "Grupo",
"signup": "Regístrarse",
"signup": "Crear cuenta",
"password": "Contraseña",
"username": "Nombre de usuario",
"files": "Archivos",
@@ -517,32 +517,32 @@
"source": "Fuente{suffix}",
"percentage": "{percentage}%",
"type": "Tipo{suffix}",
"size": "Talla{suffix}",
"size": "Tamaño{suffix}",
"body": "Cuerpo{suffix}",
"home": "Inicio{suffix}",
"clear": "Claro{suffix}",
"clear": "Limpiar{suffix}",
"help": "Ayuda{suffix}",
"login": "Inicio de sesión{suffix}",
"logout": "Cierre de sesión{suffix}",
"login": "Iniciar sesión{suffix}",
"logout": "Cerrar sesión{suffix}",
"preview": "Vista previa{suffix}",
"settings": "Ajustes{suffix}",
"sources": "Fuentes{suffix}",
"links": "Enlaces{suffix}",
"links": "Accesos directos{suffix}",
"tool": "Herramienta{suffix}",
"used": "usado{suffix}",
"status": "Estado{suffix}",
"analyze": "Analice{suffix}",
"analyze": "Analizar{suffix}",
"results": "Resultados{suffix}",
"users": "Usuarios{suffix}",
"photos": "Fotos",
"audio": "Audio{suffix}",
"videos": "Vídeos{suffix}",
"documents": "Documentos{suffix}",
"archives": "Archivos{suffix}",
"archives": "Archivos comprimidos{suffix}",
"images": "Imágenes{suffix}",
"music": "Música{suffix}",
"pdf": "PDF{suffix}",
"search": "Buscar en{suffix}",
"search": "Buscar{suffix}",
"number": "Número{suffix}",
"types": "Tipos{suffix}",
"video": "Vídeo{suffix}",
@@ -552,22 +552,22 @@
"documentation": "documentación{suffix}",
"examples": "Ejemplos{suffix}",
"dark": "Oscuro{suffix}",
"light": "Luz{suffix}",
"light": "Claro{suffix}",
"title": "Tema{suffix}",
"expires": "Expira en{suffix}",
"scope": "Alcance{suffix}",
"rules": "Reglas{suffix}",
"downloads": "Descargas{suffix}",
"submit": "Inicio de sesión{suffix}",
"submit": "Aceptar{suffix}",
"navigation": "Navegación{suffix}",
"metadata": "Metadatos{suffix}",
"loading": "Cargando{suffix}",
"metadata": "Metadata{suffix}",
"loading": "Cargando...{suffix}",
"add": "Añadir{suffix}",
"cancel": "Cancelar{suffix}",
"verify": "Verifique{suffix}",
"verify": "Verificar{suffix}",
"close": "Cerrar{suffix}",
"copy": "Copia{suffix}",
"create": "Cree{suffix}",
"copy": "Copiar{suffix}",
"create": "Crear{suffix}",
"delete": "Borrar{suffix}",
"download": "Descargar{suffix}",
"file": "Archivo{suffix}",
@@ -577,65 +577,65 @@
"move": "Mover{suffix}",
"new": "Nuevo{suffix}",
"next": "Siguiente{suffix}",
"ok": "OK{suffix}",
"ok": "ok{suffix}",
"previous": "Anterior{suffix}",
"publish": "Publique{suffix}",
"rename": "Cambie el nombre de{suffix}",
"replace": "Sustituir{suffix}",
"publish": "Publicar{suffix}",
"rename": "Cambiar nombre{suffix}",
"replace": "Reemplazar{suffix}",
"save": "Guardar{suffix}",
"schedule": "Horario{suffix}",
"select": "Seleccione{suffix}",
"select": "Seleccionar{suffix}",
"share": "Compartir{suffix}",
"update": "Actualización{suffix}",
"upload": "Cargar{suffix}",
"openFile": "Abrir expediente{suffix}",
"continue": "Continúe en{suffix}",
"update": "Actualizar{suffix}",
"upload": "Subir Archivos{suffix}",
"openFile": "Abrir Archivo{suffix}",
"continue": "Continuar{suffix}",
"edit": "Editar{suffix}",
"pause": "Pausa{suffix}",
"resume": "Currículum{suffix}",
"pause": "Pausar{suffix}",
"resume": "Reanudar{suffix}",
"retry": "Reintentar{suffix}",
"disable": "Desactivar{suffix}",
"confirm": "Confirme{suffix}",
"stay": "Permanezca en{suffix}",
"scopes": "Visores{suffix}",
"confirm": "Confirmar{suffix}",
"stay": "Quedarme{suffix}",
"scopes": "Alcances{suffix}",
"commands": "Comandos{suffix}",
"uploading": "Cargar{suffix}",
"uploading": "Subiendo{suffix}",
"paused": "En pausa{suffix}",
"conflict": "Conflicto{suffix}",
"change": "Cambia{suffix}"
"change": "Cambiar{suffix}"
},
"access": {
"rules": "Normas de acceso",
"deny": "Denegar",
"deniedUsers": "Usuarios denegados",
"deniedGroups": "Grupos denegados",
"rules": "Reglas de acceso",
"deny": "Bloquear",
"deniedUsers": "Usuarios bloqueados",
"deniedGroups": "Grupos bloqueados",
"allowedUsers": "Usuarios permitidos",
"allowedGroups": "Grupos permitidos",
"allow": "Permitir",
"allowDeny": "Permitir/Denegar",
"allowDeny": "Permitir/Bloquear",
"userGroup": "Usuario/Grupo",
"enterName": "Escribe el nombre del usuario, o del grupo",
"enterName": "Escribe el nombre del usuario o grupo",
"deleted": "Regla de acceso eliminada",
"added": "Regla de acceso añadida",
"accessManagement": "Gestión de accesos",
"totalDenied": "Denegado",
"totalAllowed": "Permitido",
"all": "Denegar todo",
"defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas -- \"Permitir\" significa que los usuarios pueden acceder (por defecto), \"Denegar\" significa que los usuarios están bloqueados (por defecto) a menos que se les permita explícitamente.",
"defaultBehavior": "Acción por defecto {suffix}",
"deletedCascade": "Reglas de acceso eliminadas de la ruta y de todas las sub-rutas.",
"cascadeDelete": "Supresión recursiva",
"cascadeDeleteDescription": "Cuando está activada, la eliminación de una regla de acceso también la eliminará de todos los subdirectorios. Esto permite eliminar completamente un usuario o grupo de todo el árbol de directorios."
"totalDenied": "Bloqueados",
"totalAllowed": "Permitidos",
"all": "Bloquear todo",
"defaultBehaviorDescription": "La regla por defecto para todos los directorios es \"Permitir\": todos los usuarios tienen acceso a todo a menos que especifiques una regla -- \"Denegar\" significa que los usuarios estarán bloqueados: No podrán acceder a esa carpeta(s) a menos que lo permitas especifcamente",
"defaultBehavior": "Regla por defecto {suffix}",
"deletedCascade": "Reglas de acceso eliminadas",
"cascadeDelete": "Eliminación en cascada",
"cascadeDeleteDescription": "Si eliminas una regla de acceso con esta opción, también la eliminará para todo el contenido (archivos y carpetas), lo cual nos permite eliminar por completo un usuario o grupo de todo el árbol de directorios"
},
"fileLoading": {
"title": "Subir archivos",
"maxConcurrentUpload": "Subidas a la vez",
"maxConcurrentUploadHelp": "El número máximo de archivos a subir a la vez. Los valores más altos suelen dar mejores resultados, pero esto depende de muchos factores",
"title": "Subida y Descarga",
"maxConcurrentUpload": "Archivos a subir a la vez",
"maxConcurrentUploadHelp": "El número máximo de archivos a subir a la vez -- Por ejemplo si el valor es 5 y subes 10 archivos, subirá los archivos de 5 en 5. Los valores más altos suelen ser mejores, pero depende de varios factores",
"uploadChunkSizeMb": "Velocidad de subida (en MB)",
"uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables.",
"clearAll": "Hacer que \"Limpiar completados\" borre todo (incluyendo error, pausa y en conflicto)",
"clearAllDescription": "El botón de borrado de \"subidas completadas\" borrará todas los archivos subidos en todos los estados: Finalizado, error, pausa y conflicto.",
"uploadSettings": "Configuracion de subidas"
"uploadChunkSizeMbHelp": "Subida de archivos fragmentada o \"Chunked uploads\" en inglés - Lo que está opción hace es controlar la velocidad de subida en la que los fragmentos de archivo se subirán.",
"clearAll": "\"Limpiar completados\" debe borrar todo",
"clearAllDescription": "Hará que el botón de \"Limpiar completados\" en la ventana de subir archivos borre todos los estados: Finalizado, error, pausa y conflicto.",
"uploadSettings": "Opciones de subida"
},
"colors": {
"red": "Rojo",
@@ -653,7 +653,7 @@
"editorQuickSave": "Botón de guardado rápido en el editor",
"editorQuickSaveDescription": "El editor de texto mostrará siempre el icono de guardar, ocultando cualquier otra acción",
"previewOffice": "Mostrar miniaturas para los archivos de Office",
"previewOfficeDescription": "Mostrar miniaturas de los tipos de archivos de Office en el tipo de vista: Lista",
"previewOfficeDescription": "Mostrar miniaturas para los archivos de Office",
"disableThumbnailPreviews": "Desactivar miniaturas para las siguientes extensiones:",
"disableThumbnailPreviewsDescription": "Una lista (separada por espacios) de extensiones de archivo para desactivar la vista previa en las miniaturas (por ejemplo, '.txt .html').",
"disableViewingFiles": "Desactivar el visor/editor de archivos para las siguientes extensiones:",
@@ -682,7 +682,7 @@
"editOffice": "Ver y editar archivos de ofimática con OnlyOffice",
"editOfficeDescription": "Cuando está activado, el visor/editor de archivos estará desactivado para todas las extensiones de archivo relacionadas con archivos office, en vez, las abrirá en OnlyOffice.",
"popupPreview": "Vista previa emergente",
"popupPreviewDescription": "Se mostrará una ventana emergente superpuesta sobre la interfaz de usuario al pasar el ratón por encima de los archivos que tengan una miniatura",
"popupPreviewDescription": "Se mostrará una ventana emergente superpuesta sobre la interfaz de usuario al pasar el mouse por encima de los archivos que tengan una miniatura",
"filePreviewOptions": "Opciones de previsualización de archivos",
"disableUpdateNotifications": "Desactivar las notificaciones de actualización",
"disableUpdateNotificationsDescription": "Desactivar el banner de notificación de actualización que aparece para los usuarios administradores en el panel lateral.",
@@ -691,7 +691,7 @@
"disableQuickToggles": "Desactivar ajustes rápidos",
"disableQuickTogglesDescription": "Desactiva los ajustes rápidos que aparecen en el panel lateral. (cambiar tema, visibilidad del panel y modo de un solo click)",
"disableSearchOptions": "Desactivar las opciones de busqueda",
"disableFileExtensions": "Escribe aquí las extensiones para deshabilitar",
"disableFileExtensions": "Escribe aquí las extensiones para desactivar",
"disableSearchOptionsDescription": "Desactivar las opciones de busqueda que aparecen en el panel.",
"hideSidebarFileActions": "Ocultar el botón de acciones de archivo en el panel",
"deleteWithoutConfirming": "Eliminar archivos sin mensaje de confirmación",
@@ -708,10 +708,10 @@
"editor": {
"uninitialized": "Hubo un error al iniciar el editor",
"saveFailed": "No se ha podido guardar el archivo. Por favor, inténtelo de nuevo.",
"saveAborted": "No se ha podido guardar el archivo. El nombre del archivo no coincide con el que estás editando.",
"saveAbortedMessage": "Operación de guardado abortada. El archivo activo ({activeFile}) no coincide con el que estas intentando guardar ({tryingToSave}).",
"syncFailed": "No se ha podido sincronizar el estado con la ruta para \"{filename}\" después de 5 intentos - Se abortará la configuración del editor para evitar la corrupción de datos.",
"fileSaved": "¡Fichero guardado!",
"saveAborted": "No se ha podido guardar el archivo.",
"saveAbortedMessage": "No se ha podido guardar el archivo. El nombre del archivo \"{activeFile}\" no coincide con el que estas intentando guardar ({tryingToSave}).",
"syncFailed": "No se ha podido encontrar la ruta para \"{filename}\" después de 5 intentos - Se cerrará el editor para evitar la corrupción de datos.",
"fileSaved": "!Se guardo el archivo correctamente!",
"errorSavingFile": "Error al guardar el archivo: {error}"
},
"player": {
@@ -727,20 +727,20 @@
"emptyQueue": "La cola de reproducción está vacia..."
},
"fileSizeAnalyzer": {
"title": "Analizador de tamaño de archivos",
"title": "Analizador de archivos",
"searchLocation": "Buscar ubicación",
"selectPath": "Seleccione el lugar de búsqueda",
"largerThan": "Mayor que (MB)",
"selectPath": "Selecciona el lugar de búsqueda",
"largerThan": "Mayor que (en MB)",
"includeFolders": "Incluir carpetas",
"includeFoldersDescription": "Incluir directorios en el análisis",
"includeFoldersDescription": "Incluir carpetas en el análisis",
"totalFiles": "Archivos totales{suffix}",
"totalSize": "Tamaño total{suffix}",
"emptyState": "Introduzca los criterios de búsqueda y haga clic en Analizar para visualizar el tamaño de los archivos",
"completeResults": "Resultados completos",
"incompleteResults": "Los resultados pueden ser incompletos",
"incompleteResultsDetails": "Mostrando resultados máximos (100). Intenta restringir la búsqueda para obtener más resultados por debajo de 100.",
"emptyState": "Introduce los criterios de búsqueda y haz clic en \"Analizar\" para visualizar el tamaño de los archivos",
"completeResults": "Análisis completo",
"incompleteResults": "Los resultados podrían estar incompletos",
"incompleteResultsDetails": "Solo se mostraran 100 resultados como máximo. Intenta restringir la búsqueda para obtener mejores resultados",
"relativePercentage": "Porcentaje relativo{suffix}",
"percentageOfResults": "{percentage}% de resultados"
"percentageOfResults": "{percentage}% de los resultados"
},
"fileTypes": {
"video": "Vídeo",
@@ -753,65 +753,65 @@
"other": "Otros"
},
"tools": {
"description": "Utilidades y herramientas de análisis para sus archivos",
"description": "Utilidades y herramientas que te seŕan de ayuda",
"sizeAnalyzer": {
"name": "Analizador de tamaño de archivos",
"description": "Visualice archivos y directorios de gran tamaño con un mapa de árbol interactivo"
"name": "Analizador de archivos",
"description": "Encuentra que archivos y carpetas están ocupando más espacio en tu disco."
},
"title": "Herramientas",
"duplicateFinder": {
"name": "Buscador de archivos duplicados",
"description": "Encuentre y gestione los archivos duplicados para recuperar espacio de almacenamiento"
"description": "Encuentra y gestiona archivos duplicados para recuperar espacio."
},
"materialIconPicker": {
"name": "Icono de selección de material",
"description": "Navega y busca Material Icons con vista previa en directo",
"name": "Material Icons",
"description": "¡Explora y descubre Iconos de la colección Material Icons!",
"searchPlaceholder": "Buscar iconos...",
"popularIcons": "Mostrar {count} iconos populares",
"showingResults": "Mostrando resultados de {count} ",
"noResults": "No se han encontrado iconos",
"tryDifferentSearch": "Pruebe con otro término de búsqueda",
"copiedToClipboard": "¡Copiado '{name}' al portapapeles!",
"copyFailed": "Error al copiar al portapapeles",
"custom": "A medida",
"popularIcons": "Mostrando {count} iconos populares",
"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",
"browseFullLibrary": "Explorar la biblioteca completa de iconos"
"browseFullLibrary": "Mira la la colección completa aqui"
},
"toolNotFound": "Herramienta no encontrada",
"toolNotFound": "No se encontró la herramienta",
"backToTools": "Volver a Herramientas"
},
"messages": {
"pathNotFound": "La ruta no existe en el sistema de archivos",
"pathNotFoundMessage": "La ruta para esto ya no existe en el sistema de archivos. Puede que se haya movido o borrado.",
"pathNotFound": "La ruta no se encuentra en el sistema de archivos",
"pathNotFoundMessage": "La ruta ya no existe en el sistema de archivos. Quizá fue movida o borrada",
"reassignPath": "Reasignar ruta",
"pathReassigned": "¡Ruta actualizada correctamente!",
"pathReassignFailed": "No se ha podido actualizar la ruta. Inténtelo de nuevo."
"pathReassigned": "¡La ruta se actualizó correctamente!",
"pathReassignFailed": "No se ha podido actualizar la ruta. Inténtalo de nuevo"
},
"duplicateFinder": {
"title": "Buscador de archivos duplicados",
"minSize": "Tamaño mínimo (MB){suffix}",
"minSizeHint": "Analizar sólo archivos mayores que este tamaño (en megabytes)",
"minSize": "Tamaño mínimo (en MB){suffix}",
"minSizeHint": "Solo analizar archivos que sean mayores que ese tamaño",
"useChecksums": "Comparar el contenido de los archivos",
"useChecksumsDescription": "Incluya también el contenido del archivo en la comparación. Sin esto, los archivos se comparan por tamaño y similitud de nombre.",
"useChecksumsDescription": "También incluir el contenido de los archivos en la comparación. Si no, los archivos se compararán solamente nombre y tmanaño",
"findDuplicates": "Buscar duplicados",
"groupsFound": "Grupos duplicados encontrados{suffix}",
"totalWastedSpace": "Espacio total desperdiciado{suffix}",
"groupsFound": "Grupos de duplicados{suffix}",
"totalWastedSpace": "Espacio desperdiciado{suffix}",
"wasted": "Desperdiciado{suffix}",
"emptyState": "Introduzca los criterios de búsqueda y haga clic en Buscar duplicados para identificar los archivos duplicados."
"emptyState": "Introduce los criterios de búsqueda y haz clic en \"Buscar duplicados\" para encontrar que archivos pueden estar gastando espacio."
},
"notifications": {
"title": "Notificaciones",
"description": "Ver todas las notificaciones de la sesión actual",
"empty": "Aún no hay notificaciones",
"description": "Aquí verás todas las notificaciones de tu sesión actual",
"empty": "Aún no tienes notificaciones",
"clearAll": "Borrar todo",
"clearConfirm": "¿Estás seguro de que quieres borrar todo el historial de notificaciones?",
"total": "{count} notificaciones"
},
"events": {
"serverShutdown": "El servidor se ha apagado.",
"authenticationFailed": "Error de autenticación. Actualice la página para volver a iniciar sesión.",
"reconnected": "Reconectado al servidor.",
"connectionLost": "Se ha perdido la conexión con el servidor. Intentando reconectar..."
"serverShutdown": "El servidor fúe apagado",
"authenticationFailed": "Hubo un error de autenticación - Actualiza la página y vuelve a iniciar sesión",
"reconnected": "!Se reconecto con el servidor!",
"connectionLost": "Se perdió la conexión con el servidor. Intentando reconectar..."
}
}
+2 -2
View File
@@ -273,7 +273,6 @@
"download": "Télécharger les fichiers"
},
"permissions-name": "Permissions",
"permissionsHelp": "Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n",
"profileSettings": "Paramètres du profil",
"ruleExample1": "bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers",
"ruleExample2": "bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "par exemple \"/sous-dossier\" - vide par défaut"
"newUserHintSubFolder": "par exemple \"/sous-dossier\" - vide par défaut",
"permissionsHelp": "Les autorisations de l'utilisateur contrôlent l'accès au type d'actions que l'utilisateur peut effectuer. L'accès administrateur permet de mettre à jour et de modifier les paramètres de tout utilisateur ainsi que de nombreux paramètres du système."
},
"sidebar": {
"siteSettings": "Paramètres du site",
+2 -2
View File
@@ -274,7 +274,6 @@
"download": "הורדת קבצים"
},
"permissions-name": "הרשאות",
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות ייבחרו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
"profileSettings": "הגדרות פרופיל",
"ruleExample1": "מנע גישה לקבצים נסתרים (כל קובץ שמתחיל בנקודה, לדוגמא .git)",
"ruleExample2": "חסימת גישה לקובץ בשם Caddyfile בתחום הראשי.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "פרוקסי"
},
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל"
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל",
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת."
},
"sidebar": {
"siteSettings": "הגדרות",
+2 -2
View File
@@ -274,7 +274,6 @@
"download": "Fájlok letöltése"
},
"permissions-name": "Jogosultságok",
"permissionsHelp": "A felhasználót beállíthatja rendszergazdának, vagy egyénileg is kiválaszthatja a jogosultságokat. Ha a \"Rendszergazda\" lehetőséget választja, az összes többi opció automatikusan be lesz jelölve. A felhasználók kezelése továbbra is a rendszergazda kiváltsága marad.\n",
"profileSettings": "Profilbeállítások",
"ruleExample1": "megakadályozza a hozzáférést bármely rejtett fájlhoz (pl. .git, .gitignore) bármely mappában.\n",
"ruleExample2": "blokkolja a hozzáférést a Caddyfile nevű fájlhoz a hatókör gyökerében.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "pl. '/subfolder' - üres az alapértelmezéshez"
"newUserHintSubFolder": "pl. '/subfolder' - üres az alapértelmezéshez",
"permissionsHelp": "A felhasználói engedélyek szabályozzák, hogy a felhasználó milyen típusú műveleteket hajthat végre. A rendszergazdai hozzáférés teljes körű ellenőrzést biztosít bármely felhasználó beállításainak, valamint számos rendszerbeállításnak a frissítéséhez és módosításához."
},
"sidebar": {
"siteSettings": "Oldalbeállítások",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Scarica i file"
},
"permissions-name": "Permessi",
"permissionsHelp": "È possibile impostare l'utente come amministratore o scegliere i permessi singolarmente. Se si seleziona \"Amministratore\", tutte le altre opzioni saranno automaticamente assegnate. La gestione degli utenti rimane un privilegio di un amministratore.\n",
"profileSettings": "Impostazioni del profilo",
"ruleExample1": "impedisci l'accesso a qualsiasi file avente come prefisso un punto\n (ad esempio .git, .gitignore) presente in ogni cartella.\n",
"ruleExample2": "blocca l'accesso al file denominato Caddyfile nella radice del campo di applicazione.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "es. '/sottocartella' - vuoto per l'impostazione predefinita"
"newUserHintSubFolder": "es. '/sottocartella' - vuoto per l'impostazione predefinita",
"permissionsHelp": "I permessi utente controllano l'accesso al tipo di azioni che l'utente può eseguire. L'accesso dell'amministratore consente di aggiornare e modificare le impostazioni di qualsiasi utente e molte impostazioni del sistema."
},
"sidebar": {
"siteSettings": "Impostazioni del sito",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "ファイルのダウンロード"
},
"permissions-name": "権限",
"permissionsHelp": "ユーザーを管理者に設定するか、権限を個別に設定できます。「管理者」を選択した場合、その他のすべての選択肢は自動的に設定されます。ユーザーの管理は管理者の権限として保持されます。",
"profileSettings": "プロファイル設定",
"ruleExample1": "各フォルダ内で名前がドットで始まるファイル(例:.git、.gitignore)へのアクセスを制限します。",
"ruleExample2": "スコープのルートパスで名前がCaddyfileのファイルへのアクセスを制限します。",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "プロキシ"
},
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白"
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白",
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。"
},
"sidebar": {
"siteSettings": "サイト設定",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "파일 다운로드"
},
"permissions-name": "권한",
"permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n",
"profileSettings": "프로필 설정",
"ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n",
"ruleExample2": "Caddyfile파일의 접근을 방지합니다.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "프록시"
},
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다."
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다.",
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다."
},
"sidebar": {
"siteSettings": "사이트 설정",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Bestanden downloaden"
},
"permissions-name": "Permissies",
"permissionsHelp": "U kunt de gebruiker instellen als beheerder of de machtigingen afzonderlijk kiezen. Als u \"Beheerder\" selecteert, worden alle andere opties automatisch gecontroleerd. Het beheer van gebruikers blijft een privilege van een beheerder.\n",
"profileSettings": "Profielinstellingen",
"ruleExample1": "voorkomt de toegang tot elk puntbestand (zoals .git, .gitignore) in elke map.\n",
"ruleExample2": "blokkeert de toegang tot het bestand met de naam Caddyfile in de hoofdmap van het bereik.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard"
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard",
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Administrar toegang geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen."
},
"sidebar": {
"siteSettings": "Site-instellingen",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Pobieranie plików"
},
"permissions-name": "Uprawnienia",
"permissionsHelp": "Możesz uczynić użytkownika administratorem, lub wybrać uprawnienia indywidualnie. Jeśli zaznaczysz opcję \"Administrator\", wszystkie pozostałe opcje zostaną automatycznie zaznaczone. Zarządzanie użytkownikami pozostaje przywilejem administratora.\n",
"profileSettings": "Twój profil",
"ruleExample1": "uniemożliwia dostęp do któregokolwiek z ukrytych plików (takich jak .git, .gitignore) w każdym folderze.\n",
"ruleExample2": "blokuje dostęp do pliku Caddyfile w głównym katalogu zakresu.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Pełnomocnik"
},
"newUserHintSubFolder": "np. '/subfolder' - domyślnie puste"
"newUserHintSubFolder": "np. '/subfolder' - domyślnie puste",
"permissionsHelp": "Uprawnienia użytkownika kontrolują dostęp do rodzaju działań, które użytkownik może wykonywać. Dostęp administratora daje pełną kontrolę nad aktualizacją i zmianą ustawień dowolnego użytkownika, a także wielu ustawień systemowych."
},
"sidebar": {
"siteSettings": "Ustawienia Strony",
+2 -2
View File
@@ -275,7 +275,6 @@
"download": "Baixar arquivos"
},
"permissions-name": "Permissões",
"permissionsHelp": "Pode definir o usuário como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos usuários é um privilégio restringido aos administradores.\n",
"profileSettings": "Configurações do usuário",
"ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n",
"ruleExample2": "bloqueia o acesso ao arquivo chamado Caddyfile.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "Por exemplo, '/subfolder' - em branco para o padrão"
"newUserHintSubFolder": "Por exemplo, '/subfolder' - em branco para o padrão",
"permissionsHelp": "As permissões de usuário controlam o acesso aos tipos de ações que o usuário pode executar. O acesso de administrador dá controle total para atualizar e alterar as configurações de qualquer usuário, bem como muitas configurações do sistema."
},
"sidebar": {
"siteSettings": "Configurações do site",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Descarregar ficheiros"
},
"permissions-name": "Permissões",
"permissionsHelp": "Pode definir o utilizador como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.\n",
"profileSettings": "Configurações do utilizador",
"ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n",
"ruleExample2": "bloqueia o acesso ao ficheiro chamado Caddyfile na raiz.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "por exemplo, '/subpasta' - em branco por defeito"
"newUserHintSubFolder": "por exemplo, '/subpasta' - em branco por defeito",
"permissionsHelp": "As permissões de utilizador controlam o acesso ao tipo de acções que o utilizador pode executar. O acesso de administrador dá controlo total para atualizar e alterar as definições de qualquer utilizador, bem como muitas definições do sistema."
},
"sidebar": {
"siteSettings": "Configurações do site",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Descărcați fișiere"
},
"permissions-name": "Permisiuni",
"permissionsHelp": "Poți alege ca un utilizator să fie administrator sau să-i alegi permisiunile individual. Dacă alegi \"Administrator\", toate celelalte opțiuni vor fi bifate automat. Gestionarea utilizatorilor rămâne un privilegiu exclusiv al administratorilor.\n",
"profileSettings": "Setări profil",
"ruleExample1": "împiedică accesul la fisiere cu punct in față (.), cum ar fi .git, .gitignore în orice director.\n",
"ruleExample2": "împiedică accesul la fișierul Caddyfile din rădăcina domeniului.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "de exemplu \"/subfolder\" - gol pentru implicit"
"newUserHintSubFolder": "de exemplu \"/subfolder\" - gol pentru implicit",
"permissionsHelp": "Permisiunile utilizatorului controlează accesul la ce tip de acțiuni poate efectua utilizatorul. Accesul de administrator oferă control deplin pentru actualizarea și modificarea setărilor oricărui utilizator, precum și a multor setări de sistem."
},
"sidebar": {
"siteSettings": "Setări site",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Скачать файлы"
},
"permissions-name": "Права доступа",
"permissionsHelp": "Можно настроить пользователя как администратора или выбрать разрешения индивидуально. При выборе \"Администратор\", все остальные параметры будут автоматически выбраны. Управление пользователями - привилегия администратора.\n",
"profileSettings": "Настройки профиля",
"ruleExample1": "Запретить доступ к любому скрытому файлу (например: .git, .gitignore) в каждой папке.\n",
"ruleExample2": "блокирует доступ к файлу с именем Caddyfile в корневой области.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Прокси-сервер"
},
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию"
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию",
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек."
},
"sidebar": {
"siteSettings": "Настройки сайта",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Stiahnite si súbory"
},
"permissions-name": "Práva",
"permissionsHelp": "Môžete nastaviť používateľa, aby bol administrátorom alebo vybrať práva jednotlivo. Ak zvolíte \"Administrator\", všetky ďalšie budú automaticky zaškrtnuté. Manažment používateľov ostáva v správe administrátora.\n",
"profileSettings": "Nastavenia profilu",
"ruleExample1": "blokuje prístup ku všetkým súborom začínajúcim bodkou (napríklad .git, .gitignore) v každom priečinku.\n",
"ruleExample2": "blokuje prístup k súborom s názvom Caddyfile v koreňovom priečinku.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "napr. '/subfolder' - prázdne pre predvolené nastavenie"
"newUserHintSubFolder": "napr. '/subfolder' - prázdne pre predvolené nastavenie",
"permissionsHelp": "Oprávnenia používateľa riadia prístup k typom akcií, ktoré môže používateľ vykonávať. Prístup administrátora poskytuje úplnú kontrolu nad aktualizáciou a zmenou nastavení ktoréhokoľvek používateľa, ako aj nad mnohými systémovými nastaveniami."
},
"sidebar": {
"siteSettings": "Nastavenia stránky",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Ladda ner filer"
},
"permissions-name": "Rättigheter",
"permissionsHelp": "Du kan ange att användaren ska vara administratör eller välja behörigheterna individuellt. Om du väljer \"administratör \" kommer alla andra alternativ att kontrolleras automatiskt. Hanteringen av användare är fortfarande ett privilegium för en administratör.\n",
"profileSettings": "Profil inställningar",
"ruleExample1": "förhindrar åtkomst till en dot-fil (till exempel. git,. gitignore) i varje mapp.\n",
"ruleExample2": "blockerar åtkomsten till filen som heter Caddyfilen i roten av scopet.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "t.ex. \"/undermapp\" - tom för standard"
"newUserHintSubFolder": "t.ex. \"/undermapp\" - tom för standard",
"permissionsHelp": "Användarbehörigheter styr vilka typer av åtgärder som användaren kan utföra. Administratörsbehörighet ger full kontroll över uppdatering och ändring av alla användarinställningar samt många systeminställningar."
},
"sidebar": {
"siteSettings": "System inställningar",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Dosyaları indirin"
},
"permissions-name": "İzinler",
"permissionsHelp": "Kullanıcıyı yönetici olarak ayarlayabilir veya izinleri ayrı ayrı seçebilirsiniz. \"Yönetici\"yi seçerseniz, diğer tüm seçenekler otomatik olarak kontrol edilecektir. Kullanıcıların yönetimi, bir yöneticinin yetkisi olarak kalır.\n",
"profileSettings": "Profil ayarları",
"ruleExample1": "her klasördeki herhangi bir noktalı dosyaya (.git, .gitignore gibi) erişimi engeller.\n",
"ruleExample2": "Root erişimidenki CaddyFile dosyalarına erişimi engelle.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Proxy"
},
"newUserHintSubFolder": "örneğin '/subfolder' - varsayılan için boş"
"newUserHintSubFolder": "örneğin '/subfolder' - varsayılan için boş",
"permissionsHelp": "Kullanıcı izinleri, kullanıcının ne tür eylemler gerçekleştirebileceğine erişimi kontrol eder. Yönetici erişimi, herhangi bir kullanıcının ayarlarının yanı sıra birçok sistem ayarını güncellemek ve değiştirmek için tam kontrol sağlar."
},
"sidebar": {
"siteSettings": "Site ayarları!",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "Завантажити файли"
},
"permissions-name": "Дозволи",
"permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"profileSettings": "Налаштування профілю",
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "Довірена особа"
},
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням"
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням",
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань."
},
"sidebar": {
"siteSettings": "Налаштування сайту",
+3 -3
View File
@@ -273,7 +273,6 @@
"download": "下载文件"
},
"permissions-name": "权限",
"permissionsHelp": "您可以将该用户设置为管理员或单独选择各项权限。如果您选择了“管理员”,则其他的选项会被自动选中,同时该用户可以管理其他用户。\n",
"profileSettings": "个人设置",
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。\n",
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "代理"
},
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空"
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空",
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。"
},
"sidebar": {
"siteSettings": "网站设置",
@@ -660,7 +660,7 @@
"publish": "出版{suffix}",
"rename": "重新命名{suffix}",
"replace": "更换{suffix}",
"save": "节省{suffix}",
"save": "保存{suffix}",
"schedule": "时间安排{suffix}",
"select": "选择{suffix}",
"share": "分享{suffix}",
+2 -2
View File
@@ -272,7 +272,6 @@
"download": "下載檔案"
},
"permissions-name": "權限",
"permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了「管理員」,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。",
"profileSettings": "個人設定",
"ruleExample1": "封鎖使用者存取所有資料夾下任何以 . 開頭的檔案(隱藏文件, 例如: .git, .gitignore)。",
"ruleExample2": "封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。",
@@ -323,7 +322,8 @@
"oidc": "OIDC",
"proxy": "代理"
},
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白"
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白",
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。"
},
"sidebar": {
"siteSettings": "網站設定",
+2 -3
View File
@@ -421,9 +421,8 @@ export const getters = {
},
officeViewingDisabled: filename => {
const ext = ' ' + getFileExtension(filename)
const hasDisabled = state.shareInfo?.disableOfficePreviewExt || state.user.disableOfficePreviewExt
if (hasDisabled) {
const disabledList = state.shareInfo?.disableOfficePreviewExt + ' ' + state.user.disableOfficePreviewExt
const disabledList = state.user.disableOnlyOfficeExt || ''
if (disabledList !== '') {
const disabledExts = ' ' + disabledList.toLowerCase()
if (disabledExts.includes(ext.toLowerCase())) {
return true
+3 -2
View File
@@ -44,9 +44,10 @@ export default function downloadFiles(items) {
async function startDownload(config, files, hash = "") {
try {
filesApi.download(config, files, hash);
await filesApi.download(config, files, hash);
notify.showSuccessToast("Downloading...");
} catch (e) {
notify.showError(`Error downloading: ${e}`);
console.error("Download failed:", e);
notify.showError(`Error downloading: ${e.message || e}`);
}
}
+11 -1
View File
@@ -7,7 +7,7 @@ export function getFileExtension(filename) {
// If no dot exists, return an empty string
if (firstDotIndex === -1) {
return ''
return ""
}
// Default: Get everything after the first dot
@@ -25,3 +25,13 @@ export function getFileExtension(filename) {
const lastDotIndex = filename.lastIndexOf('.')
return filename.substring(lastDotIndex)
}
export function removePrefix(filename, prefix = "") {
if (filename === undefined) {
return ""
}
if (filename.startsWith(prefix)) {
filename = filename.slice(prefix.length);
}
return filename;
}
-27
View File
@@ -48,7 +48,6 @@ export default {
removePrefix,
getApiPath,
extractSourceFromPath,
fixDownloadURL,
base64Encode,
joinPath,
goToItem,
@@ -193,29 +192,3 @@ export function goToItem(source, path, previousHistoryItem) {
export function doubleEncode(str) {
return encodeURIComponent(encodeURIComponent(str));
}
/**
* Fixes download URLs by replacing everything before /public/api
* with the current window.location.origin + globalVars.baseURL
* This is only needed when the backend didn't use externalUrl
* @param {string} downloadUrl - The original download URL from backend
* @returns {string} - The corrected URL using current client origin
*/
export function fixDownloadURL(downloadUrl) {
if (!downloadUrl) {
return downloadUrl;
}
// Find the position of /public/api in the URL
const publicApiIndex = downloadUrl.indexOf('/public/api');
if (publicApiIndex === -1) {
// If /public/api is not found, return the original URL
return downloadUrl;
}
// Extract the part from /public/api onwards
const publicApiPath = downloadUrl.substring(publicApiIndex);
// Build the corrected URL using current client origin and globalVars.baseURL
const correctedBaseURL = removeTrailingSlash(globalVars.baseURL);
return `${window.location.origin}${correctedBaseURL}${publicApiPath}`;
}
+1 -1
View File
@@ -308,7 +308,7 @@ export default {
showLineNumbers: true,
theme: this.isDarkMode ? "ace/theme/tomorrow_night_bright" : "ace/theme/github",
readOnly: this.editorReadOnly,
wrap: false,
wrap: state?.wrapEditor || false,
enableMobileMenu: !this.viewerMode,
useWorker: false,
scrollPastEnd: 0.5,
+2 -1
View File
@@ -471,7 +471,7 @@ export default {
styles['--list-icon-image-size'] = `${iconImageSize}em`;
} else {
// Normal view
styles['--item-width'] = `calc(${(100 / this.numColumns).toFixed(2)}% - 1em)`;
styles['--item-width'] = `calc(${(100 / this.numColumns)}% - 1em)`;
styles['--item-height'] = 'auto';
}
@@ -956,6 +956,7 @@ export default {
console.error("Error moving/copying items:", error);
} finally {
mutations.setLoading("listing", false);
mutations.setReload(true);
}
};
+15 -17
View File
@@ -88,9 +88,9 @@
</i>
</div>
<div class="form-flex-group">
<input class="input form-form flat-right"
<input class="input form-form flat-right disable-viewing"
:class="{ 'form-invalid': !validateExtensions(formDisablePreviews) }" type="text"
:placeholder="$t('profileSettings.disableFileExtensions')" id="disablePreviews"
:placeholder="$t('profileSettings.disableFileExtensions')"
v-model="formDisablePreviews" />
<button type="button" class="button form-button flat-left" @click="submitDisablePreviewsChange">
{{ $t("general.save") }}
@@ -107,9 +107,9 @@
</i>
</div>
<div class="form-flex-group">
<input class="input form-form flat-right"
<input class="input form-form flat-right disable-viewing"
:class="{ 'form-invalid': !validateExtensions(formDisabledViewing) }" type="text"
:placeholder="$t('profileSettings.disableFileExtensions')" id="disableViewing"
:placeholder="$t('profileSettings.disableFileExtensions')"
v-model="formDisabledViewing" />
<button type="button" class="button form-button flat-left" @click="submitDisabledViewingChange">
{{ $t("general.save") }}
@@ -127,10 +127,10 @@
</div>
<div class="form-flex-group">
<input class="input form-form flat-right"
:class="{ 'form-invalid': !validateExtensions(formDisableOfficePreview) }" type="text"
:placeholder="$t('profileSettings.disableFileExtensions')" id="disableOfficePreview"
v-model="formDisableOfficePreview" />
<button type="button" class="button form-button flat-left" @click="submitDisableOfficePreviewChange">
:class="{ 'form-invalid': !validateExtensions(formDisableOfficeViewing) }" type="text"
:placeholder="$t('profileSettings.disableFileExtensions')" id="disableOfficeViewing"
v-model="formDisableOfficeViewing" />
<button type="button" class="button form-button flat-left" @click="submitDisableOfficeViewingChange">
{{ $t("general.save") }}
</button>
</div>
@@ -191,7 +191,7 @@ export default {
localuser: { preview: {}, permissions: {} }, // Initialize localuser with empty objects to avoid undefined errors
formDisablePreviews: "", // holds temporary input before saving
formDisabledViewing: "", // holds temporary input before saving
formDisableOfficePreview: "", // holds temporary input before saving
formDisableOfficeViewing: "", // holds temporary input before saving
expandedSection: 'listingOptions', // Track which section is currently expanded for accordion behavior
};
},
@@ -243,7 +243,7 @@ export default {
}
this.formDisablePreviews = this.localuser.disablePreviewExt;
this.formDisabledViewing = this.localuser.disableViewingExt;
this.formDisableOfficePreview = this.localuser.disableOfficePreviewExt;
this.formDisableOfficeViewing = this.localuser.disableOnlyOfficeExt;
},
methods: {
showTooltip(event, text) {
@@ -279,12 +279,12 @@ export default {
this.localuser.disableViewingExt = this.formDisabledViewing;
this.updateSettings();
},
submitDisableOfficePreviewChange() {
if (!this.validateExtensions(this.formDisableOfficePreview)) {
submitDisableOfficeViewingChange() {
if (!this.validateExtensions(this.formDisableOfficeViewing)) {
notify.showError("Invalid input, does not match requirement.");
return;
}
this.localuser.disableOfficePreviewExt = this.formDisableOfficePreview;
this.localuser.disableOnlyOfficeExt = this.formDisableOfficeViewing;
this.updateSettings();
},
setColor(string) {
@@ -317,7 +317,7 @@ export default {
"quickDownload",
"disablePreviewExt",
"disableViewingExt",
"disableOfficePreviewExt",
"disableOnlyOfficeExt",
"deleteWithoutConfirming",
"preview",
"disableQuickToggles",
@@ -358,9 +358,7 @@ export default {
</script>
<style scoped>
#disablePreviews,
#disableViewing,
#disableOfficePreview {
.disable-viewing {
width: 80%;
}
+5 -16
View File
@@ -57,7 +57,7 @@
</button>
</td>
<td class="small">
<button :disabled="item.shareType == 'upload'" class="action copy-clipboard" :data-clipboard-text="fixDownloadURL(item.downloadURL)" v-if="item.downloadURL"
<button :disabled="item.shareType == 'upload'" class="action copy-clipboard" :data-clipboard-text="item.downloadURL" v-if="item.downloadURL"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')">
<i class="material-icons">content_paste_go</i>
</button>
@@ -74,14 +74,12 @@
<script>
import { notify } from "@/notify";
import { publicApi, shareApi } from "@/api";
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 { fixDownloadURL } from "@/utils/url";
import { globalVars } from "@/utils/constants";
export default {
name: "shares",
@@ -200,19 +198,10 @@ export default {
return fromNow(time);
},
/**
* @param {any} item
* @param {any} share
*/
buildLink(item) {
return publicApi.getShareURL(item);
},
fixDownloadURL(downloadUrl) {
// Only fix the URL if it doesn't already have the correct external domain
if (globalVars.externalUrl) {
// URL already has the correct external domain, use as-is
return downloadUrl;
}
// URL needs fixing (internal domain or no externalUrl set)
return fixDownloadURL(downloadUrl);
buildLink(share) {
return share.shareURL;
},
},
};
@@ -0,0 +1,200 @@
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
2024年 06月 17日 星期一 04:05:58 CST 文件已备份
@@ -64,6 +64,7 @@ test("share file creation", async ({ page, checkForErrors, openContextMenu }) =>
}
await page.goto("public/share/" + shareHashShare);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - share");
await page.waitForTimeout(1000);
await openContextMenu();
await page.locator('button[aria-label="New file"]').click();
await page.locator('input[aria-label="FileName Field"]').waitFor({ state: 'visible' });